killbill-aplcache
Changes
account/pom.xml 2(+1 -1)
account/src/main/resources/org/killbill/billing/account/migration/V20161208173107__parent_id_idx.sql 1(+1 -0)
api/pom.xml 2(+1 -1)
beatrix/pom.xml 2(+1 -1)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java 30(+0 -30)
beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationWithCatalogUpdate.java 97(+92 -5)
bin/import-account 2(+1 -1)
catalog/pom.xml 4(+3 -1)
catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseBillingAlignment.java 13(+13 -0)
catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanAlignment.java 17(+17 -0)
catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanPolicy.java 37(+32 -5)
catalog/src/main/resources/EmptyCatalog.xml 10(+10 -0)
catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java 66(+66 -0)
currency/pom.xml 2(+1 -1)
entitlement/pom.xml 2(+1 -1)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java 29(+19 -10)
entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java 21(+15 -6)
entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java 2(+1 -1)
entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java 4(+2 -2)
entitlement/src/main/resources/org/killbill/billing/entitlement/migration/V20161208173547__blocking_states_id_idx.sql 1(+1 -0)
entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java 35(+35 -0)
invoice/pom.xml 2(+1 -1)
invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java 51(+47 -4)
invoice/src/main/resources/org/killbill/billing/invoice/migration/V20161208173933__invoice_payment_idx.sql 2(+2 -0)
invoice/src/main/resources/org/killbill/billing/invoice/migration/V20164030010821__invoice_default_status_459.sql 1(+1 -0)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java 411(+313 -98)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java 643(+642 -1)
invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java 137(+137 -0)
invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java 2(+1 -1)
jaxrs/pom.xml 2(+1 -1)
junction/pom.xml 2(+1 -1)
junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java 2(+1 -1)
junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java 2(+2 -0)
NEWS 6(+6 -0)
overdue/pom.xml 2(+1 -1)
payment/pom.xml 2(+1 -1)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java 83(+35 -48)
payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java 13(+10 -3)
payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java 37(+37 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java 32(+27 -5)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java 44(+33 -11)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java 4(+3 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java 47(+47 -0)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java 11(+10 -1)
payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java 42(+38 -4)
payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java 167(+144 -23)
payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java 13(+10 -3)
payment/src/main/resources/org/killbill/billing/payment/migration/V20161130055443__payment_external_keys_not_null.sql 4(+4 -0)
payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java 106(+106 -0)
payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java 4(+4 -0)
payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java 6(+3 -3)
pom.xml 4(+2 -2)
profiles/killbill/pom.xml 2(+1 -1)
profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties 168(+130 -38)
profiles/killpay/pom.xml 2(+1 -1)
profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties 168(+130 -38)
profiles/pom.xml 2(+1 -1)
subscription/pom.xml 2(+1 -1)
subscription/src/main/resources/org/killbill/billing/subscription/migration/V20161130055443__bundles_external_key_not_null.sql 1(+1 -0)
tenant/pom.xml 2(+1 -1)
usage/pom.xml 2(+1 -1)
usage/src/main/resources/org/killbill/billing/usage/migration/V20161130005717__add_tracking_id_index_502.sql 1(+1 -0)
util/pom.xml 2(+1 -1)
Details
account/pom.xml 2(+1 -1)
diff --git a/account/pom.xml b/account/pom.xml
index ba0b8a9..08a0cc5 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-account</artifactId>
diff --git a/account/src/main/resources/org/killbill/billing/account/ddl.sql b/account/src/main/resources/org/killbill/billing/account/ddl.sql
index 031e560..a24f272 100644
--- a/account/src/main/resources/org/killbill/billing/account/ddl.sql
+++ b/account/src/main/resources/org/killbill/billing/account/ddl.sql
@@ -35,6 +35,7 @@ CREATE TABLE accounts (
) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
CREATE UNIQUE INDEX accounts_id ON accounts(id);
CREATE UNIQUE INDEX accounts_external_key ON accounts(external_key, tenant_record_id);
+CREATE INDEX accounts_parents ON accounts(parent_account_id);
CREATE INDEX accounts_tenant_record_id ON accounts(tenant_record_id);
DROP TABLE IF EXISTS account_history;
diff --git a/account/src/main/resources/org/killbill/billing/account/migration/V20161208173107__parent_id_idx.sql b/account/src/main/resources/org/killbill/billing/account/migration/V20161208173107__parent_id_idx.sql
new file mode 100644
index 0000000..b018110
--- /dev/null
+++ b/account/src/main/resources/org/killbill/billing/account/migration/V20161208173107__parent_id_idx.sql
@@ -0,0 +1 @@
+CREATE INDEX accounts_parents ON accounts(parent_account_id);
api/pom.xml 2(+1 -1)
diff --git a/api/pom.xml b/api/pom.xml
index 8936827..f024002 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-internal-api</artifactId>
beatrix/pom.xml 2(+1 -1)
diff --git a/beatrix/pom.xml b/beatrix/pom.xml
index c752c0d..af68cf9 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-beatrix</artifactId>
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
index f7fb6b3..ff36bb8 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/overdue/TestOverdueBase.java
@@ -21,26 +21,18 @@ package org.killbill.billing.beatrix.integration.overdue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.UUID;
-import java.util.concurrent.Callable;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.beatrix.integration.BeatrixIntegrationModule;
import org.killbill.billing.beatrix.integration.TestIntegrationBase;
import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.entitlement.api.BlockingState;
-import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.SubscriptionBundle;
-import org.killbill.billing.overdue.OverdueService;
import org.killbill.billing.overdue.config.DefaultOverdueConfig;
-import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.payment.api.PaymentMethodPlugin;
import org.killbill.billing.payment.api.TestPaymentMethodPluginBase;
import org.killbill.xmlloader.XMLLoader;
-import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
-import static com.jayway.awaitility.Awaitility.await;
-import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.Assert.assertNotNull;
public abstract class TestOverdueBase extends TestIntegrationBase {
@@ -78,26 +70,4 @@ public abstract class TestOverdueBase extends TestIntegrationBase {
protected void checkODState(final String expected) {
checkODState(expected, account.getId());
}
-
- protected void checkODState(final String expected, final UUID accountId) {
- try {
- // This will test the overdue notification queue: when we move the clock, the overdue system
- // should get notified to refresh its state.
- // Calling explicitly refresh here (overdueApi.refreshOverdueStateFor(account)) would not fully
- // test overdue.
- // Since we're relying on the notification queue, we may need to wait a bit (hence await()).
- await().atMost(10, SECONDS).until(new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
- final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
- return expected.equals(stateName);
- }
- });
- } catch (final Exception e) {
- final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
- final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
- Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
- }
- }
}
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
index bab10ab..7697a94 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestIntegrationBase.java
@@ -24,6 +24,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.Callable;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -56,6 +57,8 @@ import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceListSet;
import org.killbill.billing.catalog.api.ProductCategory;
+import org.killbill.billing.entitlement.api.BlockingState;
+import org.killbill.billing.entitlement.api.BlockingStateType;
import org.killbill.billing.entitlement.api.DefaultEntitlement;
import org.killbill.billing.entitlement.api.Entitlement;
import org.killbill.billing.entitlement.api.EntitlementApi;
@@ -76,11 +79,14 @@ import org.killbill.billing.lifecycle.api.Lifecycle;
import org.killbill.billing.lifecycle.glue.BusModule;
import org.killbill.billing.mock.MockAccountBuilder;
import org.killbill.billing.osgi.config.OSGIConfig;
+import org.killbill.billing.overdue.OverdueService;
import org.killbill.billing.overdue.api.OverdueApi;
import org.killbill.billing.overdue.api.OverdueConfig;
import org.killbill.billing.overdue.caching.OverdueConfigCache;
import org.killbill.billing.overdue.listener.OverdueListener;
+import org.killbill.billing.overdue.wrapper.OverdueWrapper;
import org.killbill.billing.overdue.wrapper.OverdueWrapperFactory;
+import org.killbill.billing.payment.api.AdminPaymentApi;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApi;
import org.killbill.billing.payment.api.PaymentApiException;
@@ -112,6 +118,7 @@ import org.killbill.billing.util.tag.Tag;
import org.killbill.bus.api.PersistentBus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@@ -126,6 +133,8 @@ import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Stage;
+import static com.jayway.awaitility.Awaitility.await;
+import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
@@ -200,6 +209,9 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
protected PaymentApi paymentApi;
@Inject
+ protected AdminPaymentApi adminPaymentApi;
+
+ @Inject
protected EntitlementApi entitlementApi;
@Inject
@@ -349,6 +361,28 @@ public class TestIntegrationBase extends BeatrixTestSuiteWithEmbeddedDB {
assertTrue(ctd.toDateTime(testTimeZone).toLocalDate().compareTo(new LocalDate(chargeThroughDate.getYear(), chargeThroughDate.getMonthOfYear(), chargeThroughDate.getDayOfMonth())) == 0);
}
+ protected void checkODState(final String expected, final UUID accountId) {
+ try {
+ // This will test the overdue notification queue: when we move the clock, the overdue system
+ // should get notified to refresh its state.
+ // Calling explicitly refresh here (overdueApi.refreshOverdueStateFor(account)) would not fully
+ // test overdue.
+ // Since we're relying on the notification queue, we may need to wait a bit (hence await()).
+ await().atMost(10, SECONDS).until(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ return expected.equals(stateName);
+ }
+ });
+ } catch (final Exception e) {
+ final BlockingState blockingStateForService = blockingApi.getBlockingStateForService(accountId, BlockingStateType.ACCOUNT, OverdueService.OVERDUE_SERVICE_NAME, internalCallContext);
+ final String stateName = blockingStateForService != null ? blockingStateForService.getStateName() : OverdueWrapper.CLEAR_STATE_NAME;
+ Assert.assertEquals(stateName, expected, "Got exception: " + e.toString());
+ }
+ }
+
protected DefaultSubscriptionBase subscriptionDataFromSubscription(final SubscriptionBase sub) {
return (DefaultSubscriptionBase) sub;
}
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 3f81642..f07eb82 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
@@ -33,6 +33,7 @@ import org.killbill.billing.api.TestApiListener.NextEvent;
import org.killbill.billing.beatrix.util.InvoiceChecker.ExpectedInvoiceItemCheck;
import org.killbill.billing.callcontext.DefaultCallContext;
import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
+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.CatalogUserApi;
@@ -48,8 +49,8 @@ import org.killbill.billing.catalog.api.TimeUnit;
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.entitlement.api.Subscription;
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;
@@ -226,8 +227,6 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
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 {
@@ -238,7 +237,6 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
assertEquals(catalog.getCurrentPlans().size(), 1);
-
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("thirty-monthly", null);
createEntitlement(spec, null, true);
@@ -274,12 +272,101 @@ public class TestIntegrationWithCatalogUpdate extends TestIntegrationBase {
}
}
+ @Test(groups = "slow")
+ public void testWith$0RecurringPlan() throws Exception {
+
+ // Create a per-tenant catalog with one plan
+ final SimplePlanDescriptor zeroDesc = new DefaultSimplePlanDescriptor("zeroDesc-monthly", "Zero", ProductCategory.BASE, account.getCurrency(), BigDecimal.ZERO, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+ catalogUserApi.addSimplePlan(zeroDesc, init, testCallContext);
+ StaticCatalog catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+ assertEquals(catalog.getCurrentPlans().size(), 1);
+
+ final PlanPhaseSpecifier specZero = new PlanPhaseSpecifier("zeroDesc-monthly", null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ final Entitlement baseEntitlement = entitlementApi.createBaseEntitlement(account.getId(), specZero, UUID.randomUUID().toString(), ImmutableList.<PlanPhasePriceOverride>of(), null, null, false, ImmutableList.<PluginProperty>of(), testCallContext);
+ assertListenerStatus();
+
+ Subscription refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 7, 1));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 8, 1));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 9, 1));
+
+ // Add another Plan in the catalog
+ final SimplePlanDescriptor descNonZero = new DefaultSimplePlanDescriptor("superfoo-monthly", "SuperFoo", ProductCategory.BASE, account.getCurrency(), new BigDecimal("20.00"), BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+ catalogUserApi.addSimplePlan(descNonZero, init, testCallContext);
+ catalog = catalogUserApi.getCurrentCatalog("dummy", testCallContext);
+ assertEquals(catalog.getCurrentPlans().size(), 2);
+
+ final PlanPhaseSpecifier specNonZero = new PlanPhaseSpecifier("superfoo-monthly", null);
+
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ final Entitlement baseEntitlement2 = entitlementApi.createBaseEntitlement(account.getId(), specNonZero, UUID.randomUUID().toString(), ImmutableList.<PlanPhasePriceOverride>of(), null, null, false, ImmutableList.<PluginProperty>of(), testCallContext);
+ assertListenerStatus();
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 10, 1));
+
+ Subscription refreshedBaseEntitlement2 = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement2.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement2.getChargedThroughDate(), new LocalDate(2016, 10, 1));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+ refreshedBaseEntitlement2 = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement2.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement2.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+ busHandler.pushExpectedEvents(NextEvent.BLOCK);
+ baseEntitlement.cancelEntitlementWithDateOverrideBillingPolicy(clock.getUTCToday(), BillingActionPolicy.END_OF_TERM, ImmutableList.<PluginProperty>of(), testCallContext);
+ assertListenerStatus();
+
+ busHandler.pushExpectedEvents(NextEvent.CANCEL, NextEvent.NULL_INVOICE, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+ refreshedBaseEntitlement2 = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement2.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement2.getChargedThroughDate(), new LocalDate(2016, 12, 1));
+
+ busHandler.pushExpectedEvents(NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
+ clock.addMonths(1);
+ assertListenerStatus();
+
+ refreshedBaseEntitlement = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement.getChargedThroughDate(), new LocalDate(2016, 11, 1));
+
+ refreshedBaseEntitlement2 = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement2.getId(), testCallContext);
+ assertEquals(refreshedBaseEntitlement2.getChargedThroughDate(), new LocalDate(2017, 1, 1));
+
+ }
+
private Entitlement createEntitlement(final String planName, final boolean expectPayment) throws EntitlementApiException {
final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(planName, null);
return createEntitlement(spec, null, expectPayment);
}
-
private Entitlement createEntitlement(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final boolean expectPayment) throws EntitlementApiException {
if (expectPayment) {
busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.INVOICE_PAYMENT, NextEvent.PAYMENT);
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
index 7444840..25c4902 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestInvoicePayment.java
@@ -17,6 +17,8 @@
package org.killbill.billing.beatrix.integration;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
@@ -24,8 +26,6 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
-import javax.annotation.Nullable;
-
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.ErrorCode;
@@ -46,12 +46,17 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePaymentType;
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.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentOptions;
import org.killbill.billing.payment.api.PaymentTransaction;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
+import org.killbill.xmlloader.XMLLoader;
+import org.mockito.Mockito;
import org.skife.jdbi.v2.Handle;
import org.skife.jdbi.v2.tweak.HandleCallback;
import org.testng.Assert;
@@ -68,8 +73,6 @@ import static org.testng.Assert.assertTrue;
public class TestInvoicePayment extends TestIntegrationBase {
-
-
@Test(groups = "slow")
public void testCancellationEOTWithInvoiceItemAdjustmentsOnInvoiceWithMultipleItems() throws Exception {
final int billingDay = 1;
@@ -129,12 +132,8 @@ public class TestInvoicePayment extends TestIntegrationBase {
Assert.assertEquals(fourthInvoice.getInvoiceItems().size(), 1);
invoiceChecker.checkInvoice(account.getId(), 4, callContext,
new ExpectedInvoiceItemCheck(new LocalDate(2016, 11, 1), new LocalDate(2016, 12, 1), InvoiceItemType.RECURRING, new BigDecimal("249.95")));
-
-
}
-
-
@Test(groups = "slow")
public void testPartialPaymentByPaymentPlugin() throws Exception {
// 2012-05-01T00:03:42.000Z
@@ -600,6 +599,415 @@ public class TestInvoicePayment extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testWithPendingPaymentThenSuccess() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ paymentPlugin.makeNextPaymentPending();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // INVOICE_PAYMENT_ERROR is sent for PENDING payments
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Verify account transitions to OD1 while payment is PENDING
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+
+ // Transition the payment to success
+ final List<String> paymentControlPluginNames = ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ final PaymentOptions paymentOptions = Mockito.mock(PaymentOptions.class);
+ Mockito.when(paymentOptions.getPaymentControlPluginNames()).thenReturn(paymentControlPluginNames);
+
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ paymentApi.notifyPendingTransactionOfStateChangedWithPaymentControl(account, payments.get(0).getTransactions().get(0).getId(), true, paymentOptions, callContext);
+ assertListenerStatus();
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+ }
+
+ @Test(groups = "slow")
+ public void testWithPendingPaymentThenFailure() throws Exception {
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ paymentPlugin.makeNextPaymentPending();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ // INVOICE_PAYMENT_ERROR is sent for PENDING payments
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PENDING);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Transition the payment to failure
+ final List<String> paymentControlPluginNames = ImmutableList.<String>of(InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ final PaymentOptions paymentOptions = Mockito.mock(PaymentOptions.class);
+ Mockito.when(paymentOptions.getPaymentControlPluginNames()).thenReturn(paymentControlPluginNames);
+
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ paymentApi.notifyPendingTransactionOfStateChangedWithPaymentControl(account, payments.get(0).getTransactions().get(0).getId(), false, paymentOptions, callContext);
+ assertListenerStatus();
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertEquals(invoice2, invoice1);
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(accountBalance1) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ // Note that because notifyPendingTransactionOfStateChangedWithPaymentControl is considered an API call, no retry will be attempted
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "ABORTED");
+ }
+
+ @Test(groups = "slow")
+ public void testWithSuccessfulPaymentFixedToFailure() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertTrue(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+
+ // Transition the payment to failure
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+ adminPaymentApi.fixPaymentTransactionState(payments.get(0), payments.get(0).getTransactions().get(0), TransactionStatus.PAYMENT_FAILURE, null, null, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertEquals(invoice2.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoice2.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice2.getPayments().get(0).isSuccess());
+ assertNotNull(invoice2.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ // Note that because fixPaymentTransactionState is considered an API call, no retry will be attempted
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "ABORTED");
+
+ // Verify account transitions to OD1
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+ }
+
+ @Test(groups = "slow")
+ public void testWithFailedPaymentFixedToSuccess() throws Exception {
+ // Verify integration with Overdue in that particular test
+ final String configXml = "<overdueConfig>" +
+ " <accountOverdueStates>" +
+ " <initialReevaluationInterval>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </initialReevaluationInterval>" +
+ " <state name=\"OD1\">" +
+ " <condition>" +
+ " <timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " <unit>DAYS</unit><number>1</number>" +
+ " </timeSinceEarliestUnpaidInvoiceEqualsOrExceeds>" +
+ " </condition>" +
+ " <externalMessage>Reached OD1</externalMessage>" +
+ " <blockChanges>true</blockChanges>" +
+ " <disableEntitlementAndChangesBlocked>false</disableEntitlementAndChangesBlocked>" +
+ " </state>" +
+ " </accountOverdueStates>" +
+ "</overdueConfig>";
+ final InputStream is = new ByteArrayInputStream(configXml.getBytes());
+ final DefaultOverdueConfig config = XMLLoader.getObjectFromStreamNoValidation(is, DefaultOverdueConfig.class);
+ overdueConfigCache.loadDefaultOverdueConfig(config);
+
+ clock.setDay(new LocalDate(2012, 4, 1));
+
+ final AccountData accountData = getAccountData(1);
+ final Account account = createAccountWithNonOsgiPaymentMethod(accountData);
+ accountChecker.checkAccount(account.getId(), accountData, callContext);
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ paymentPlugin.makeNextPaymentFailWithError();
+
+ final DefaultEntitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), "bundleKey", "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+
+ addDaysAndCheckForCompletion(30, NextEvent.PHASE, NextEvent.INVOICE, NextEvent.PAYMENT_ERROR, NextEvent.INVOICE_PAYMENT_ERROR);
+
+ invoiceChecker.checkChargedThroughDate(baseEntitlement.getId(), new LocalDate(2012, 6, 1), callContext);
+
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId(), false, callContext);
+ assertEquals(invoices.size(), 2);
+
+ final Invoice invoice1 = invoices.get(0).getInvoiceItems().get(0).getInvoiceItemType() == InvoiceItemType.RECURRING ?
+ invoices.get(0) : invoices.get(1);
+ assertTrue(invoice1.getBalance().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice1.getPaidAmount().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice1.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice1.getPayments().size(), 1);
+ assertEquals(invoice1.getPayments().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(invoice1.getPayments().get(0).getCurrency(), Currency.USD);
+ assertFalse(invoice1.getPayments().get(0).isSuccess());
+ assertNotNull(invoice1.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance1 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance1.compareTo(new BigDecimal("249.95")) == 0);
+
+ final List<Payment> payments = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments.size(), 1);
+ assertEquals(payments.get(0).getPurchasedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().size(), 1);
+ assertEquals(payments.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedAmount().compareTo(BigDecimal.ZERO), 0);
+ assertEquals(payments.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ assertEquals(payments.get(0).getPaymentAttempts().size(), 2);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(0).getStateName(), "RETRIED");
+ assertEquals(payments.get(0).getPaymentAttempts().get(1).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments.get(0).getPaymentAttempts().get(1).getStateName(), "SCHEDULED");
+
+ // Verify account transitions to OD1
+ addDaysAndCheckForCompletion(2, NextEvent.BLOCK);
+ checkODState("OD1", account.getId());
+
+ // Transition the payment to success
+ final PaymentTransaction existingPaymentTransaction = payments.get(0).getTransactions().get(0);
+ final PaymentTransaction updatedPaymentTransaction = Mockito.mock(PaymentTransaction.class);
+ Mockito.when(updatedPaymentTransaction.getId()).thenReturn(existingPaymentTransaction.getId());
+ Mockito.when(updatedPaymentTransaction.getExternalKey()).thenReturn(existingPaymentTransaction.getExternalKey());
+ Mockito.when(updatedPaymentTransaction.getTransactionType()).thenReturn(existingPaymentTransaction.getTransactionType());
+ Mockito.when(updatedPaymentTransaction.getProcessedAmount()).thenReturn(new BigDecimal("249.95"));
+ Mockito.when(updatedPaymentTransaction.getProcessedCurrency()).thenReturn(existingPaymentTransaction.getCurrency());
+ busHandler.pushExpectedEvents(NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT, NextEvent.BLOCK);
+ adminPaymentApi.fixPaymentTransactionState(payments.get(0), updatedPaymentTransaction, TransactionStatus.SUCCESS, null, null, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ checkODState(OverdueWrapper.CLEAR_STATE_NAME, account.getId());
+
+ final Invoice invoice2 = invoiceUserApi.getInvoice(invoice1.getId(), callContext);
+ assertTrue(invoice2.getBalance().compareTo(BigDecimal.ZERO) == 0);
+ assertTrue(invoice2.getPaidAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertTrue(invoice2.getChargedAmount().compareTo(new BigDecimal("249.95")) == 0);
+ assertEquals(invoice2.getPayments().size(), 1);
+ assertEquals(invoice2.getPayments().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(invoice2.getPayments().get(0).getCurrency(), Currency.USD);
+ assertTrue(invoice2.getPayments().get(0).isSuccess());
+ assertNotNull(invoice2.getPayments().get(0).getPaymentId());
+
+ final BigDecimal accountBalance2 = invoiceUserApi.getAccountBalance(account.getId(), callContext);
+ assertTrue(accountBalance2.compareTo(BigDecimal.ZERO) == 0);
+
+ final List<Payment> payments2 = paymentApi.getAccountPayments(account.getId(), false, true, ImmutableList.<PluginProperty>of(), callContext);
+ assertEquals(payments2.size(), 1);
+ assertEquals(payments2.get(0).getPurchasedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().size(), 1);
+ assertEquals(payments2.get(0).getTransactions().get(0).getAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedAmount().compareTo(new BigDecimal("249.95")), 0);
+ assertEquals(payments2.get(0).getTransactions().get(0).getProcessedCurrency(), Currency.USD);
+ assertEquals(payments2.get(0).getTransactions().get(0).getTransactionStatus(), TransactionStatus.SUCCESS);
+ assertEquals(payments2.get(0).getPaymentAttempts().size(), 1);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getPluginName(), InvoicePaymentControlPluginApi.PLUGIN_NAME);
+ assertEquals(payments2.get(0).getPaymentAttempts().get(0).getStateName(), "SUCCESS");
+ }
+
+ @Test(groups = "slow")
public void testWithIncompletePaymentAttempt() throws Exception {
// 2012-05-01T00:03:42.000Z
clock.setTime(new DateTime(2012, 5, 1, 0, 3, 42, 0));
diff --git a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v1.xml b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v1.xml
index 4701c94..5a977b6 100644
--- a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v1.xml
+++ b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v1.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>END_OF_TERM</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>END_OF_TERM</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
diff --git a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v2.xml b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v2.xml
index d75891d..c575b09 100644
--- a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v2.xml
+++ b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v2.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>END_OF_TERM</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>END_OF_TERM</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
diff --git a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v3.xml b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v3.xml
index 8ad8ba4..63acb70 100644
--- a/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v3.xml
+++ b/beatrix/src/test/resources/retiredCatalogs/WeaponsHireSmall-v3.xml
@@ -66,12 +66,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>END_OF_TERM</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>END_OF_TERM</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
bin/import-account 2(+1 -1)
diff --git a/bin/import-account b/bin/import-account
index aca6838..1d3460c 100755
--- a/bin/import-account
+++ b/bin/import-account
@@ -90,7 +90,7 @@ function export_data() {
function import_data() {
local filename=$1
local columns_names=$2
- mysqlimport --local --ignore-lines=1 --fields-terminated-by=, --fields-enclosed-by=\" --columns=$columns_names --verbose -u$USERNAME -p$PASSWORD $DATABASE $TMP_DIR/$filename
+ mysql --local-infile --execute="LOAD DATA LOCAL INFILE '$TMP_DIR/$filename' INTO TABLE $filename FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' IGNORE 1 LINES ($columns_names); SHOW WARNINGS" -u$USERNAME -p$PASSWORD $DATABASE
}
function sanitize_and_import() {
catalog/pom.xml 4(+3 -1)
diff --git a/catalog/pom.xml b/catalog/pom.xml
index 49aad72..c5f4978 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-catalog</artifactId>
@@ -38,6 +38,8 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
+ <!-- Required scope for the maven shade plugin (tools) -->
+ <scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
index 2adf38c..5c6482d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/api/user/DefaultCatalogUserApi.java
@@ -17,12 +17,17 @@
package org.killbill.billing.catalog.api.user;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.net.URISyntaxException;
import javax.inject.Inject;
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
import org.joda.time.DateTime;
+import org.killbill.billing.ErrorCode;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.CatalogUpdater;
import org.killbill.billing.catalog.StandaloneCatalog;
@@ -33,6 +38,7 @@ import org.killbill.billing.catalog.api.Catalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.CatalogService;
import org.killbill.billing.catalog.api.CatalogUserApi;
+import org.killbill.billing.catalog.api.InvalidConfigException;
import org.killbill.billing.catalog.api.SimplePlanDescriptor;
import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.catalog.caching.CatalogCache;
@@ -42,9 +48,11 @@ 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.ValidationException;
import org.killbill.xmlloader.XMLLoader;
+import org.xml.sax.SAXException;
-public class DefaultCatalogUserApi implements CatalogUserApi {
+public class DefaultCatalogUserApi implements CatalogUserApi {
private final CatalogService catalogService;
private final InternalCallContextFactory internalCallContextFactory;
@@ -77,19 +85,34 @@ public class DefaultCatalogUserApi implements CatalogUserApi {
@Override
public void uploadCatalog(final String catalogXML, final CallContext callContext) throws CatalogApiException {
+
+
+ final InternalTenantContext internalTenantContext = createInternalTenantContext(callContext);
try {
// Validation purpose: Will throw if bad XML or catalog validation fails
final InputStream stream = new ByteArrayInputStream(catalogXML.getBytes());
XMLLoader.getObjectFromStream(new URI("dummy"), stream, StandaloneCatalog.class);
- final InternalTenantContext internalTenantContext = createInternalTenantContext(callContext);
catalogCache.clearCatalog(internalTenantContext);
tenantApi.addTenantKeyValue(TenantKey.CATALOG.toString(), catalogXML, callContext);
- } catch (TenantApiException e) {
+ } catch (final TenantApiException e) {
throw new CatalogApiException(e);
- } catch (final Exception e) {
+ } catch (final ValidationException e) {
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_FOR_TENANT, internalTenantContext.getTenantRecordId());
+ } catch (final JAXBException e) {
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_FOR_TENANT, internalTenantContext.getTenantRecordId());
+ } catch (final IOException e) {
+ throw new IllegalStateException(e);
+ } catch (final TransformerException e) {
+ throw new IllegalStateException(e);
+ } catch (final URISyntaxException e) {
+ throw new IllegalStateException(e);
+ } catch (final SAXException e) {
+ throw new IllegalStateException(e);
+ } catch (final InvalidConfigException e) {
throw new IllegalStateException(e);
}
+
}
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 6350016..897d725 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
@@ -31,6 +31,7 @@ import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
import org.killbill.billing.catalog.DefaultTierPriceOverride;
import org.killbill.billing.catalog.DefaultTieredBlockPriceOverride;
import org.killbill.billing.catalog.DefaultUsagePriceOverride;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.Plan;
@@ -105,7 +106,9 @@ public class EhCacheOverriddenPlanCache implements OverriddenPlanCache {
final List<CatalogOverridePhaseDefinitionModelDao> phaseDefs = overrideDao.getOverriddenPlanPhases(planDefRecordId, context);
final DefaultPlan defaultPlan = (DefaultPlan) catalog.findCurrentPlan(parentPlanName);
final PlanPhasePriceOverride[] overrides = createOverrides(defaultPlan, phaseDefs, context);
- return new DefaultPlan(planName, defaultPlan, overrides);
+ final DefaultPlan result = new DefaultPlan(planName, defaultPlan, overrides);
+ result.initialize((StandaloneCatalog) catalog, ((StandaloneCatalog) catalog).getCatalogURI());
+ return result;
}
private PlanPhasePriceOverride[] createOverrides(final Plan defaultPlan, final List<CatalogOverridePhaseDefinitionModelDao> phaseDefs, final InternalTenantContext context) {
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/CatalogSafetyInitializer.java b/catalog/src/main/java/org/killbill/billing/catalog/CatalogSafetyInitializer.java
new file mode 100644
index 0000000..68d90d8
--- /dev/null
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogSafetyInitializer.java
@@ -0,0 +1,129 @@
+/*
+ * 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.catalog;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+
+import org.killbill.billing.catalog.api.BlockType;
+import org.killbill.billing.catalog.api.FixedType;
+
+public class CatalogSafetyInitializer {
+
+
+ public static final Integer DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE = -1;
+
+ private static final Map<Class, LinkedList<Field>> perCatalogClassNonRequiredFields = new HashMap<Class, LinkedList<Field>>();
+
+ //
+ // Ensure that all uninitialized arrays for which there is neither a 'required' XmlElementWrapper or XmlElement annotation
+ // end up initialized with a default zero length array (allowing to safely get the length and iterate over (0) element.
+ //
+ public static void initializeNonRequiredNullFieldsWithDefaultValue(final Object obj) {
+
+ LinkedList<Field> fields = perCatalogClassNonRequiredFields.get(obj.getClass());
+ if (fields == null) {
+ fields = initializeNonRequiredFields(obj.getClass());
+ perCatalogClassNonRequiredFields.put(obj.getClass(), fields);
+ }
+ try {
+ for (final Field f : fields) {
+ if (f.getType().isArray()) {
+ initializeArrayIfNull(obj, f);
+ } else if (!f.getType().isPrimitive()) {
+ if (f.getType().isEnum()) {
+ if (FixedType.class.equals(f.getType())) {
+ initializeFieldWithValue(obj, f, FixedType.ONE_TIME);
+ } else if (BlockType.class.equals(f.getType())) {
+ initializeFieldWithValue(obj, f, BlockType.VANILLA);
+ }
+ } else if (Integer.class.equals(f.getType())) {
+ initializeFieldWithValue(obj, f, DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE);
+ }
+ }
+ }
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException("Failed during catalog initialization : ", e);
+ } catch (final ClassNotFoundException e) {
+ throw new RuntimeException("Failed during catalog initialization : ", e);
+ }
+ }
+
+ // For each type of catalog object we keep the 'Field' associated to non required attribute fields
+ private static LinkedList<Field> initializeNonRequiredFields(final Class<?> aClass) {
+
+ final LinkedList<Field> result = new LinkedList();
+ final Field[] fields = aClass.getDeclaredFields();
+ for (final Field f : fields) {
+ if (f.getType().isArray()) {
+ final XmlElementWrapper xmlElementWrapper = f.getAnnotation(XmlElementWrapper.class);
+ if (xmlElementWrapper != null) {
+ if (!xmlElementWrapper.required()) {
+ result.add(f);
+ }
+ } else {
+ final XmlElement xmlElement = f.getAnnotation(XmlElement.class);
+ if (xmlElement != null && !xmlElement.required()) {
+ result.add(f);
+ }
+ }
+ } else if (!f.getType().isPrimitive()) {
+ if (f.getType().isEnum()) {
+ if (FixedType.class.equals(f.getType())) {
+ result.add(f);
+ } else if (BlockType.class.equals(f.getType())) {
+ result.add(f);
+ }
+ } else if (Integer.class.equals(f.getType())) {
+ result.add(f);
+ }
+ }
+ }
+ return result;
+ }
+
+ private static void initializeFieldWithValue(final Object obj, final Field f, final Object value) throws IllegalAccessException, ClassNotFoundException {
+ f.setAccessible(true);
+ if (f.get(obj) == null) {
+ f.set(obj, value);
+ }
+ f.setAccessible(false);
+ }
+
+ private static void initializeArrayIfNull(final Object obj, final Field f) throws IllegalAccessException, ClassNotFoundException {
+ f.setAccessible(true);
+ if (f.get(obj) == null) {
+ f.set(obj, getZeroLengthArrayInitializer(f));
+ }
+ f.setAccessible(false);
+ }
+
+
+ private static Object[] getZeroLengthArrayInitializer(final Field f) throws ClassNotFoundException {
+ // Yack... type erasure, why?
+ final String arrayClassName = f.getType().getCanonicalName();
+ final Class type = Class.forName(arrayClassName.substring(0, arrayClassName.length() - 2));
+ return (Object[]) Array.newInstance(type, 0);
+ }
+}
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 f8b61b6..9701a73 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/CatalogUpdater.java
@@ -28,6 +28,7 @@ import org.killbill.billing.catalog.api.BillingAlignment;
import org.killbill.billing.catalog.api.BillingMode;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.InternationalPrice;
import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanAlignmentChange;
@@ -80,6 +81,8 @@ public class CatalogUpdater {
.setPlanRules(getSaneDefaultPlanRules(defaultPriceList));
if (currencies != null && currencies.length > 0) {
tmp.setSupportedCurrencies(currencies);
+ } else {
+ tmp.setSupportedCurrencies(new Currency[0]);
}
tmp.initialize(tmp, DUMMY_URI);
@@ -168,9 +171,7 @@ public class CatalogUpdater {
evergreenPhase.setRecurring(recurring);
}
- try {
- recurring.getRecurringPrice().getPrice(desc.getCurrency());
- } catch (CatalogApiException ignore) {
+ if (!isPriceForCurrencyExists(recurring.getRecurringPrice(), desc.getCurrency())) {
catalog.addRecurringPriceToPlan(recurring.getRecurringPrice(), new DefaultPrice().setCurrency(desc.getCurrency()).setValue(desc.getAmount()));
}
@@ -194,6 +195,18 @@ public class CatalogUpdater {
catalog.initialize(catalog, DUMMY_URI);
}
+ private boolean isPriceForCurrencyExists(final InternationalPrice price, final Currency currency) {
+ if (price.getPrices().length == 0) {
+ return false;
+ }
+ try {
+ price.getPrice(currency);
+ } catch (CatalogApiException ignore) {
+ return false;
+ }
+ return true;
+ }
+
private void validateExistingPlan(final DefaultPlan plan, final SimplePlanDescriptor desc) throws CatalogApiException {
boolean failedValidation = false;
@@ -266,7 +279,7 @@ public class CatalogUpdater {
private void validateNewPlanDescriptor(final SimplePlanDescriptor desc) throws CatalogApiException {
if (desc.getProductCategory() == null ||
desc.getBillingPeriod() == null ||
- (desc.getAmount() == null || desc.getAmount().compareTo(BigDecimal.ZERO) <= 0) ||
+ (desc.getAmount() == null || desc.getAmount().compareTo(BigDecimal.ZERO) < 0) ||
desc.getCurrency() == null) {
throw new CatalogApiException(ErrorCode.CAT_INVALID_SIMPLE_PLAN_DESCRIPTOR, desc);
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
index 46bee2d..6f05fd6 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultBlock.java
@@ -89,6 +89,11 @@ public class DefaultBlock extends ValidatingConfig<StandaloneCatalog> implements
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ // Safety check
+ if (type == null) {
+ throw new IllegalStateException("type should have been automatically been initialized with VANILLA ");
+ }
+
if (type == BlockType.TOP_UP && minTopUpCredit == null) {
errors.add(new ValidationError(String.format("TOP_UP block needs to define minTopUpCredit for phase %s",
phase.getName()), catalog.getCatalogURI(), DefaultUsage.class, ""));
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
index cc1e343..b7e5fd4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultDuration.java
@@ -16,6 +16,8 @@
package org.killbill.billing.catalog;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@@ -35,7 +37,6 @@ import org.killbill.xmlloader.ValidationErrors;
@XmlAccessorType(XmlAccessType.NONE)
public class DefaultDuration extends ValidatingConfig<StandaloneCatalog> implements Duration {
- public static final int DEFAULT_DURATION_NUMBER = -1;
@XmlElement(required = true)
private TimeUnit unit;
@@ -59,7 +60,6 @@ public class DefaultDuration extends ValidatingConfig<StandaloneCatalog> impleme
}
public DefaultDuration() {
- number = DEFAULT_DURATION_NUMBER;
}
@Override
@@ -102,16 +102,30 @@ public class DefaultDuration extends ValidatingConfig<StandaloneCatalog> impleme
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
- //Validation: TimeUnit UNLIMITED iff number == -1
- if ((unit == TimeUnit.UNLIMITED && number != -1)) {
- errors.add(new ValidationError("Duration can only have 'UNLIMITED' unit if the number is omitted.",
- catalog.getCatalogURI(), DefaultPlanPhase.class, ""));
+
+ // Safety check
+ if (number == null) {
+ throw new IllegalStateException("number should have been automatically been initialized with DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE ");
}
- //TODO MDW - Validation TimeUnit UNLIMITED iff number == -1
+ //Validation: TimeUnit UNLIMITED if number == -1
+ if ((unit == TimeUnit.UNLIMITED && number != CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE)) {
+ errors.add(new ValidationError("Duration can only have 'UNLIMITED' unit if the number is omitted",
+ catalog.getCatalogURI(), DefaultDuration.class, ""));
+ } else if ((unit != TimeUnit.UNLIMITED) && number == CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE) {
+ errors.add(new ValidationError("Finite Duration must have a well defined length",
+ catalog.getCatalogURI(), DefaultDuration.class, ""));
+ }
return errors;
}
+ @Override
+ public void initialize(final StandaloneCatalog root, final URI uri) {
+ super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
public DefaultDuration setUnit(final TimeUnit unit) {
this.unit = unit;
return this;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultFixed.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultFixed.java
index 8d214f8..924fbf5 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultFixed.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultFixed.java
@@ -50,7 +50,6 @@ public class DefaultFixed extends ValidatingConfig<StandaloneCatalog> implements
}
public DefaultFixed() {
- type = FixedType.ONE_TIME;
}
public DefaultFixed(final DefaultFixed in, final PlanPhasePriceOverride override) {
@@ -60,6 +59,8 @@ public class DefaultFixed extends ValidatingConfig<StandaloneCatalog> implements
@Override
public void initialize(final StandaloneCatalog root, final URI uri) {
+ super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
if (fixedPrice != null) {
fixedPrice.initialize(root, uri);
}
@@ -67,6 +68,10 @@ public class DefaultFixed extends ValidatingConfig<StandaloneCatalog> implements
@Override
public ValidationErrors validate(final StandaloneCatalog root, final ValidationErrors errors) {
+ // Safety check
+ if (type == null) {
+ throw new IllegalStateException("fixedPrice should have been automatically been initialized with ONE_TIME ");
+ }
return errors;
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java
index 1514278..d2b5f7c 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultInternationalPrice.java
@@ -37,9 +37,8 @@ import org.killbill.xmlloader.ValidationErrors;
@XmlAccessorType(XmlAccessType.NONE)
public class DefaultInternationalPrice extends ValidatingConfig<StandaloneCatalog> implements InternationalPrice {
- //TODO: Must have a price point for every configured currency
- //TODO: No prices is a zero cost plan
- @XmlElement(name = "price")
+ // No prices is a zero cost plan in all currencies
+ @XmlElement(name = "price", required = false)
private DefaultPrice[] prices;
@@ -54,14 +53,20 @@ public class DefaultInternationalPrice extends ValidatingConfig<StandaloneCatalo
public DefaultInternationalPrice() {}
public DefaultInternationalPrice(final DefaultInternationalPrice in, final PlanPhasePriceOverride override, final boolean fixed) {
- this.prices = in.getPrices() != null ? new DefaultPrice[in.getPrices().length] : null;
- // There is a question on whether we keep the other prices that were not overridden or only have one entry for the overridden price on that currency.
- for (int i = 0; i < in.getPrices().length; i++) {
- final DefaultPrice curPrice = (DefaultPrice) in.getPrices()[i];
- if (curPrice.getCurrency().equals(override.getCurrency())) {
- prices[i] = new DefaultPrice(fixed ? override.getFixedPrice() : override.getRecurringPrice(), override.getCurrency());
- } else {
- prices[i] = curPrice;
+
+ if (in.getPrices().length == 0) {
+ this.prices = new DefaultPrice[1];
+ this.prices[0] = new DefaultPrice(fixed ? override.getFixedPrice() : override.getRecurringPrice(), override.getCurrency());
+ } else {
+ this.prices = new DefaultPrice[in.getPrices().length];
+ // There is a question on whether we keep the other prices that were not overridden or only have one entry for the overridden price on that currency.
+ for (int i = 0; i < in.getPrices().length; i++) {
+ final DefaultPrice curPrice = (DefaultPrice) in.getPrices()[i];
+ if (curPrice.getCurrency().equals(override.getCurrency())) {
+ prices[i] = new DefaultPrice(fixed ? override.getFixedPrice() : override.getRecurringPrice(), override.getCurrency());
+ } else {
+ prices[i] = curPrice;
+ }
}
}
}
@@ -83,6 +88,10 @@ public class DefaultInternationalPrice extends ValidatingConfig<StandaloneCatalo
*/
@Override
public BigDecimal getPrice(final Currency currency) throws CatalogApiException {
+ if (prices.length == 0) {
+ return BigDecimal.ZERO;
+ }
+
for (final Price p : prices) {
if (p.getCurrency() == currency) {
return p.getValue();
@@ -128,23 +137,10 @@ public class DefaultInternationalPrice extends ValidatingConfig<StandaloneCatalo
@Override
public void initialize(final StandaloneCatalog root, final URI uri) {
- if (prices == null) {
- prices = getZeroPrice(root);
- }
super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
}
- private synchronized DefaultPrice[] getZeroPrice(final StandaloneCatalog root) {
- final Currency[] currencies = root.getCurrentSupportedCurrencies();
- final DefaultPrice[] zeroPrice = new DefaultPrice[currencies.length];
- for (int i = 0; i < currencies.length; i++) {
- zeroPrice[i] = new DefaultPrice();
- zeroPrice[i].setCurrency(currencies[i]);
- zeroPrice[i].setValue(new BigDecimal(0));
- }
-
- return zeroPrice;
- }
@Override
public boolean isZero() {
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
index 29390bf..30176b5 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultLimit.java
@@ -18,6 +18,8 @@
package org.killbill.billing.catalog;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@@ -77,6 +79,13 @@ public class DefaultLimit extends ValidatingConfig<StandaloneCatalog> implements
}
@Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
+ @Override
public boolean compliesWith(double value) {
if (max != null) {
if (value > max.doubleValue()) {
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 84fe628..5f2e9ea 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlan.java
@@ -56,7 +56,6 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
@XmlID
private String name;
- //TODO MDW Validation - effectiveDateForExistingSubscriptions > catalog effectiveDate
@XmlElement(required = false)
private Date effectiveDateForExistingSubscriptions;
@@ -76,7 +75,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;
private String priceListName;
@@ -129,7 +128,7 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
@Override
public PlanPhase[] getAllPhases() {
- final int length = (initialPhases == null || initialPhases.length == 0) ? 1 : (initialPhases.length + 1);
+ final int length = initialPhases.length + 1;
final PlanPhase[] allPhases = new DefaultPlanPhase[length];
int cnt = 0;
if (length > 1) {
@@ -175,15 +174,15 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
@Override
public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+
if (finalPhase != null) {
finalPhase.setPlan(this);
finalPhase.initialize(catalog, sourceURI);
}
- if (initialPhases != null) {
- for (final DefaultPlanPhase p : initialPhases) {
- p.setPlan(this);
- p.initialize(catalog, sourceURI);
- }
+ for (final DefaultPlanPhase p : initialPhases) {
+ p.setPlan(this);
+ p.initialize(catalog, sourceURI);
}
this.priceListName = this.priceListName != null ? this.priceListName : findPriceListForPlan(catalog);
}
@@ -194,12 +193,34 @@ public class DefaultPlan extends ValidatingConfig<StandaloneCatalog> implements
catalog.getEffectiveDate().getTime() > effectiveDateForExistingSubscriptions.getTime()) {
errors.add(new ValidationError(String.format("Price effective date %s is before catalog effective date '%s'",
effectiveDateForExistingSubscriptions,
- catalog.getEffectiveDate().getTime()),
- catalog.getCatalogURI(), DefaultInternationalPrice.class, ""));
+ catalog.getEffectiveDate()),
+ catalog.getCatalogURI(), DefaultPlan.class, ""));
+ }
+
+ if (product == null) {
+ errors.add(new ValidationError(String.format("Invalid product for plan '%s'", name), catalog.getCatalogURI(), DefaultPlan.class, ""));
+ }
+
+ for (DefaultPlanPhase cur : initialPhases) {
+ cur.validate(catalog, errors);
+ if (cur.getPhaseType() == PhaseType.EVERGREEN || cur.getPhaseType() == PhaseType.FIXEDTERM) {
+ errors.add(new ValidationError(String.format("Initial Phase %s of plan %s cannot be of type %s",
+ cur.getName(), name, cur.getPhaseType()),
+ catalog.getCatalogURI(), DefaultPlan.class, ""));
+ }
}
- validateCollection(catalog, errors, initialPhases);
finalPhase.validate(catalog, errors);
+
+ if (finalPhase.getPhaseType() == PhaseType.TRIAL || finalPhase.getPhaseType() == PhaseType.DISCOUNT) {
+ errors.add(new ValidationError(String.format("Final Phase %s of plan %s cannot be of type %s",
+ finalPhase.getName(), name, finalPhase.getPhaseType()),
+ catalog.getCatalogURI(), DefaultPlan.class, ""));
+ }
+ // Safety check
+ if (plansAllowedInBundle == null) {
+ throw new IllegalStateException("plansAllowedInBundle should have been automatically been initialized with DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE (-1)");
+ }
return errors;
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
index 6c6c47f..b2bd6cb 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPlanPhase.java
@@ -154,6 +154,10 @@ public class DefaultPlanPhase extends ValidatingConfig<StandaloneCatalog> implem
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ if (plan == null) {
+ errors.add(new ValidationError(String.format("Invalid plan for phase '%s'", type), catalog.getCatalogURI(), DefaultPlanPhase.class, ""));
+ }
+
if (fixed == null && recurring == null && usages.length == 0) {
errors.add(new ValidationError(String.format("Phase %s of plan %s need to define at least either a fixed or recurrring or usage section.",
type.toString(), plan.getName()),
@@ -165,12 +169,18 @@ public class DefaultPlanPhase extends ValidatingConfig<StandaloneCatalog> implem
if (recurring != null) {
recurring.validate(catalog, errors);
}
+ duration.validate(catalog, errors);
+
validateCollection(catalog, errors, usages);
return errors;
}
@Override
public void initialize(final StandaloneCatalog root, final URI uri) {
+
+ super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+
if (fixed != null) {
fixed.initialize(root, uri);
}
@@ -179,12 +189,11 @@ public class DefaultPlanPhase extends ValidatingConfig<StandaloneCatalog> implem
recurring.setPlan(plan);
recurring.setPhase(this);
}
- if (usages != null) {
- for (DefaultUsage usage : usages) {
- usage.initialize(root, uri);
- usage.setPhase(this);
- }
+ for (DefaultUsage usage : usages) {
+ usage.initialize(root, uri);
+ usage.setPhase(this);
}
+ duration.initialize(root, uri);
}
public DefaultPlanPhase setFixed(final DefaultFixed fixed) {
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java
index c9e0e96..069ccb6 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPrice.java
@@ -20,6 +20,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import java.math.BigDecimal;
+import java.net.URI;
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.catalog.api.CurrencyValueNull;
@@ -77,9 +78,15 @@ public class DefaultPrice extends ValidatingConfig<StandaloneCatalog> implements
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
return errors;
+ }
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
}
+
@Override
public boolean equals(final Object o) {
if (this == o) {
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
index 9bdf070..a67783a 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceList.java
@@ -16,6 +16,7 @@
package org.killbill.billing.catalog;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -42,7 +43,7 @@ public class DefaultPriceList extends ValidatingConfig<StandaloneCatalog> implem
@XmlID
private String name;
- @XmlElementWrapper(name = "plans", required = false)
+ @XmlElementWrapper(name = "plans", required = true)
@XmlIDREF
@XmlElement(type=DefaultPlan.class, name = "plan", required = false)
private CatalogEntityCollection<Plan> plans;
@@ -97,6 +98,13 @@ public class DefaultPriceList extends ValidatingConfig<StandaloneCatalog> implem
return errors;
}
+
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
public DefaultPriceList setName(final String name) {
this.name = name;
return this;
@@ -133,4 +141,10 @@ public class DefaultPriceList extends ValidatingConfig<StandaloneCatalog> implem
result = 31 * result + (plans != null ? plans.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultPriceList{" +
+ "name='" + name + '}';
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
index c159833..4aaf3cb 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultPriceListSet.java
@@ -19,6 +19,8 @@ package org.killbill.billing.catalog;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
+
+import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -104,6 +106,13 @@ public class DefaultPriceListSet extends ValidatingConfig<StandaloneCatalog> imp
return errors;
}
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
public DefaultPriceList getDefaultPricelist() {
return defaultPricelist;
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultProduct.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultProduct.java
index ffb8e96..af95ceb 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultProduct.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultProduct.java
@@ -32,6 +32,7 @@ import org.killbill.billing.catalog.api.Limit;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.xmlloader.ValidatingConfig;
+import org.killbill.xmlloader.ValidationError;
import org.killbill.xmlloader.ValidationErrors;
@XmlAccessorType(XmlAccessType.NONE)
@@ -129,7 +130,7 @@ public class DefaultProduct extends ValidatingConfig<StandaloneCatalog> implemen
protected Limit findLimit(String unit) {
- for(Limit limit: limits) {
+ for (Limit limit: limits) {
if(limit.getUnit().getName().equals(unit) ) {
return limit;
}
@@ -147,14 +148,23 @@ public class DefaultProduct extends ValidatingConfig<StandaloneCatalog> implemen
}
-
+
@Override
public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ for (DefaultLimit cur : limits) {
+ cur.initialize(catalog, sourceURI);
+ }
catalogName = catalog.getCatalogName();
}
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ if (catalogName == null || !catalogName.equals(catalog.getCatalogName())) {
+ errors.add(new ValidationError(String.format("Invalid catalogName for product '%s'", name), catalog.getCatalogURI(), DefaultProduct.class, ""));
+
+ }
//TODO: MDW validation: inclusion and exclusion lists can only contain addon products
//TODO: MDW validation: a given product can only be in, at most, one of inclusion and exclusion lists
return errors;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultRecurring.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultRecurring.java
index 57418b5..d4c180b 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultRecurring.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultRecurring.java
@@ -64,6 +64,8 @@ public class DefaultRecurring extends ValidatingConfig<StandaloneCatalog> implem
@Override
public void initialize(final StandaloneCatalog root, final URI uri) {
+ super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
if (recurringPrice != null) {
recurringPrice.initialize(root, uri);
}
@@ -72,6 +74,16 @@ public class DefaultRecurring extends ValidatingConfig<StandaloneCatalog> implem
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
// Validation: check for nulls
+
+ if (plan == null) {
+ errors.add(new ValidationError(String.format("Invalid plan for recurring section"), catalog.getCatalogURI(), DefaultRecurring.class, ""));
+ }
+
+ if (phase == null) {
+ errors.add(new ValidationError(String.format("Invalid phase for recurring section"), catalog.getCatalogURI(), DefaultPlan.class, plan.getName().toString()));
+ }
+
+
if (billingPeriod == null) {
errors.add(new ValidationError(String.format("Recurring section of Phase %s of plan %s has a recurring price but no billing period", phase.getPhaseType().toString(), plan.getName()),
catalog.getCatalogURI(), DefaultPlanPhase.class, phase.getPhaseType().toString()));
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultTier.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultTier.java
index b5e6c49..b14a148 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultTier.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultTier.java
@@ -17,6 +17,7 @@
package org.killbill.billing.catalog;
+import java.net.URI;
import java.util.Arrays;
import javax.xml.bind.annotation.XmlAccessType;
@@ -164,6 +165,14 @@ public class DefaultTier extends ValidatingConfig<StandaloneCatalog> implements
}
@Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
+
+ @Override
public boolean equals(final Object o) {
if (this == o) {
return true;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java
index a0b2170..f82499d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUnit.java
@@ -16,6 +16,8 @@
package org.killbill.billing.catalog;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
@@ -45,6 +47,14 @@ public class DefaultUnit extends ValidatingConfig<StandaloneCatalog> implements
return errors;
}
+
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
public DefaultUnit setName(final String name) {
this.name = name;
return this;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
index 570b18e..d0dba33 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/DefaultUsage.java
@@ -217,6 +217,9 @@ public class DefaultUsage extends ValidatingConfig<StandaloneCatalog> implements
@Override
public void initialize(final StandaloneCatalog root, final URI uri) {
+ super.initialize(root, uri);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+
for (DefaultLimit limit : limits) {
limit.initialize(root, uri);
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
index 80217c8..edfdf88 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
@@ -19,6 +19,7 @@
package org.killbill.billing.catalog.io;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
@@ -27,22 +28,33 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
+import javax.xml.bind.JAXBException;
+import javax.xml.transform.TransformerException;
+
import org.killbill.billing.ErrorCode;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.StandaloneCatalogWithPriceOverride;
import org.killbill.billing.catalog.VersionedCatalog;
import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.InvalidConfigException;
import org.killbill.billing.catalog.override.PriceOverride;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.clock.Clock;
import org.killbill.xmlloader.UriAccessor;
+import org.killbill.xmlloader.ValidationErrors;
+import org.killbill.xmlloader.ValidationException;
import org.killbill.xmlloader.XMLLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.SAXException;
import com.google.common.io.Resources;
import com.google.inject.Inject;
public class VersionedCatalogLoader implements CatalogLoader {
+ private static final Logger logger = LoggerFactory.getLogger(VersionedCatalogLoader.class);
+
private static final Object PROTOCOL_FOR_FILE = "file";
private static final String XML_EXTENSION = ".xml";
@@ -75,9 +87,21 @@ public class VersionedCatalogLoader implements CatalogLoader {
final StandaloneCatalog catalog = XMLLoader.getObjectFromUri(u, StandaloneCatalog.class);
result.add(new StandaloneCatalogWithPriceOverride(catalog, priceOverride, InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID, internalCallContextFactory));
}
+ // Perform initialization and validation for VersionedCatalog
+ XMLLoader.initializeAndValidate(new URI(uriString), result);
return result;
+ } catch (final ValidationException e) {
+ logger.warn("Failed to load default catalog", e);
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_DEFAULT, uriString);
+ } catch (final JAXBException e) {
+ logger.warn("Failed to load default catalog", e);
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_DEFAULT, uriString);
+ } catch(IllegalArgumentException e) {
+ logger.warn("Failed to load default catalog", e);
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_DEFAULT, uriString);
} catch (Exception e) {
- throw new CatalogApiException(ErrorCode.CAT_INVALID_DEFAULT, "Problem encountered loading catalog: ", e.getMessage());
+ logger.warn("Failed to load default catalog", e);
+ throw new IllegalStateException(e);
}
}
@@ -103,11 +127,30 @@ public class VersionedCatalogLoader implements CatalogLoader {
result.add(new StandaloneCatalogWithPriceOverride(catalog, priceOverride, tenantRecordId, internalCallContextFactory));
}
}
+ // Perform initialization and validation for VersionedCatalog
+ XMLLoader.initializeAndValidate(uri, result);
return result;
- } catch (final CatalogApiException e) {
- throw e;
- } catch (final Exception e) {
- throw new CatalogApiException(ErrorCode.CAT_INVALID_FOR_TENANT, tenantRecordId);
+ } catch (final ValidationException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_FOR_TENANT, tenantRecordId);
+ } catch (final JAXBException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new CatalogApiException(e, ErrorCode.CAT_INVALID_FOR_TENANT, tenantRecordId);
+ } catch (final IOException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new IllegalStateException(e);
+ } catch (final TransformerException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new IllegalStateException(e);
+ } catch (final URISyntaxException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new IllegalStateException(e);
+ } catch (final SAXException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new IllegalStateException(e);
+ } catch (final InvalidConfigException e) {
+ logger.warn("Failed to load catalog for tenantRecordId='{}'", tenantRecordId, e);
+ throw new IllegalStateException(e);
}
}
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 225cf7c..edf0252 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
@@ -34,6 +34,7 @@ import org.killbill.billing.catalog.DefaultPlanPhasePriceOverride;
import org.killbill.billing.catalog.DefaultTierPriceOverride;
import org.killbill.billing.catalog.DefaultTieredBlockPriceOverride;
import org.killbill.billing.catalog.DefaultUsagePriceOverride;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
@@ -70,7 +71,7 @@ public class DefaultPriceOverride implements PriceOverride {
}
@Override
- public DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, @Nullable final InternalCallContext context) throws CatalogApiException {
+ public DefaultPlan getOrCreateOverriddenPlan(final StandaloneCatalog standaloneCatalog, 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;
@@ -127,6 +128,7 @@ public class DefaultPriceOverride implements PriceOverride {
}
final DefaultPlan result = new DefaultPlan(planName, (DefaultPlan) parentPlan, resolvedOverride);
+ result.initialize(standaloneCatalog, standaloneCatalog.getCatalogURI());
if (context == null) {
overriddenPlanCache.addDryRunPlan(planName, result);
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/override/PriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/override/PriceOverride.java
index 49cdbc8..cb81b5b 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/override/PriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/override/PriceOverride.java
@@ -23,6 +23,8 @@ import org.joda.time.DateTime;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.DefaultPlan;
+import org.killbill.billing.catalog.StandaloneCatalog;
+import org.killbill.billing.catalog.StandaloneCatalogWithPriceOverride;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
@@ -30,7 +32,7 @@ import org.killbill.billing.catalog.api.StaticCatalog;
public interface PriceOverride {
- DefaultPlan getOrCreateOverriddenPlan(final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, final InternalCallContext context) throws CatalogApiException;
+ DefaultPlan getOrCreateOverriddenPlan(final StandaloneCatalog catalog, final Plan parentPlan, final DateTime catalogEffectiveDate, final List<PlanPhasePriceOverride> overrides, final InternalCallContext context) throws CatalogApiException;
DefaultPlan getOverriddenPlan(final String planName, final StaticCatalog catalog, final InternalTenantContext context) throws CatalogApiException;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
index 37184b7..83f939b 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/plugin/StandaloneCatalogMapper.java
@@ -76,7 +76,6 @@ import org.killbill.billing.catalog.rules.DefaultPlanRules;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
public class StandaloneCatalogMapper {
@@ -138,7 +137,7 @@ public class StandaloneCatalogMapper {
public DefaultCaseChangePlanPolicy apply(final CaseChangePlanPolicy input) {
return toDefaultCaseChangePlanPolicy(input);
}
- }, true);
+ });
}
final DefaultCaseChangePlanAlignment[] toDefaultCaseChangePlanAlignments(final Iterable<CaseChangePlanAlignment> input) {
@@ -147,7 +146,7 @@ public class StandaloneCatalogMapper {
public DefaultCaseChangePlanAlignment apply(final CaseChangePlanAlignment input) {
return toDefaultCaseChangePlanAlignment(input);
}
- }, true);
+ });
}
final DefaultCaseBillingAlignment[] toDefaultCaseBillingAlignments(final Iterable<CaseBillingAlignment> input) {
@@ -156,7 +155,7 @@ public class StandaloneCatalogMapper {
public DefaultCaseBillingAlignment apply(final CaseBillingAlignment input) {
return toDefaultCaseBillingAlignment(input);
}
- }, true);
+ });
}
final DefaultCaseCancelPolicy[] toDefaultCaseCancelPolicys(final Iterable<CaseCancelPolicy> input) {
@@ -165,7 +164,7 @@ public class StandaloneCatalogMapper {
public DefaultCaseCancelPolicy apply(final CaseCancelPolicy input) {
return toDefaultCaseCancelPolicy(input);
}
- }, true);
+ });
}
final DefaultCaseCreateAlignment[] toDefaultCaseCreateAlignments(final Iterable<CaseCreateAlignment> input) {
@@ -174,7 +173,7 @@ public class StandaloneCatalogMapper {
public DefaultCaseCreateAlignment apply(final CaseCreateAlignment input) {
return toCaseCreateAlignment(input);
}
- }, true);
+ });
}
final DefaultCasePriceList[] toDefaultCasePriceLists(final Iterable<CasePriceList> input) {
@@ -183,7 +182,7 @@ public class StandaloneCatalogMapper {
public DefaultCasePriceList apply(final CasePriceList input) {
return toDefaultCasePriceList(input);
}
- }, true);
+ });
}
final DefaultCasePriceList toDefaultCasePriceList(final CasePriceList input) {
@@ -318,7 +317,7 @@ public class StandaloneCatalogMapper {
public DefaultPlanPhase apply(final PlanPhase input) {
return toDefaultPlanPhase(input);
}
- }, false);
+ });
}
private DefaultPriceList[] toDefaultPriceLists(final Iterable<PriceList> input) {
@@ -327,7 +326,7 @@ public class StandaloneCatalogMapper {
public DefaultPriceList apply(final PriceList input) {
return toDefaultPriceList(input);
}
- }, true);
+ });
}
private DefaultPrice[] toDefaultPrices(final Iterable<Price> input) {
@@ -336,7 +335,7 @@ public class StandaloneCatalogMapper {
public DefaultPrice apply(final Price input) {
return toDefaultPrice(input);
}
- }, false);
+ });
}
private DefaultUnit[] toDefaultUnits(final Iterable<Unit> input) {
@@ -345,7 +344,7 @@ public class StandaloneCatalogMapper {
public DefaultUnit apply(final Unit inputTransform) {
return toDefaultUnit(inputTransform);
}
- }, true);
+ });
}
private DefaultUnit toDefaultUnit(final Unit input) {
@@ -458,8 +457,8 @@ public class StandaloneCatalogMapper {
}
}
- private <I, C extends I> C[] toArrayWithTransform(final Iterable<I> input, final Function<I, C> transformer, boolean returnNullIfNothing) {
- if (returnNullIfNothing && (input == null || !input.iterator().hasNext())) {
+ private <I, C extends I> C[] toArrayWithTransform(final Iterable<I> input, final Function<I, C> transformer) {
+ if (input == null || !input.iterator().hasNext()) {
return null;
}
final Iterable<C> tmp = Iterables.transform(input, transformer);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java b/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java
index bc25842..08571ef 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/PriceListDefault.java
@@ -16,6 +16,8 @@
package org.killbill.billing.catalog;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@@ -45,6 +47,12 @@ public class PriceListDefault extends DefaultPriceList {
}
@Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+ @Override
public String getName() {
return PriceListSet.DEFAULT_PRICELIST_NAME;
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCase.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCase.java
index de0bef2..99e23d4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCase.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCase.java
@@ -17,7 +17,9 @@
package org.killbill.billing.catalog.rules;
-import org.killbill.billing.catalog.DefaultPrice;
+import java.net.URI;
+
+import org.killbill.billing.catalog.CatalogSafetyInitializer;
import org.killbill.billing.catalog.DefaultPriceList;
import org.killbill.billing.catalog.DefaultProduct;
import org.killbill.billing.catalog.StandaloneCatalog;
@@ -92,6 +94,12 @@ public abstract class DefaultCase<T> extends ValidatingConfig<StandaloneCatalog>
return errors;
}
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
protected abstract DefaultCase<T> setProduct(Product product);
protected abstract DefaultCase<T> setProductCategory(ProductCategory productCategory);
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseBillingAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseBillingAlignment.java
index fc2b8f5..31e56fc 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseBillingAlignment.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseBillingAlignment.java
@@ -74,4 +74,17 @@ public class DefaultCaseBillingAlignment extends DefaultCasePhase<BillingAlignme
result = 31 * result + (alignment != null ? alignment.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseBillingAlignment {" +
+ "alignment=" + alignment +
+ ", phaseType=" + getPhaseType() +
+ ", product=" + getProduct() +
+ ", productCategory=" + getProductCategory() +
+ ", billingPeriod=" + getBillingPeriod() +
+ ", priceList=" + getPriceList() +
+ '}';
+ }
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
index b1a061c..11a3399 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCancelPolicy.java
@@ -74,4 +74,17 @@ public class DefaultCaseCancelPolicy extends DefaultCasePhase<BillingActionPolic
result = 31 * result + (policy != null ? policy.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseCancelPolicy{" +
+ "policy =" + policy +
+ ", phaseType =" + getPhaseType() +
+ ", product=" + getProduct() +
+ ", productCategory=" + getProductCategory() +
+ ", billingPeriod=" + getBillingPeriod() +
+ ", priceList=" + getPriceList() +
+ '}';
+ }
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChange.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChange.java
index ea441bd..d522bff 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChange.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChange.java
@@ -16,11 +16,14 @@
package org.killbill.billing.catalog.rules;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlIDREF;
+import org.killbill.billing.catalog.CatalogSafetyInitializer;
import org.killbill.billing.catalog.DefaultPriceList;
import org.killbill.billing.catalog.DefaultProduct;
import org.killbill.billing.catalog.StandaloneCatalog;
@@ -149,6 +152,12 @@ public abstract class DefaultCaseChange<T> extends ValidatingConfig<StandaloneCa
return errors;
}
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
public DefaultCaseChange<T> setPhaseType(final PhaseType phaseType) {
this.phaseType = phaseType;
return this;
@@ -294,4 +303,19 @@ public abstract class DefaultCaseChange<T> extends ValidatingConfig<StandaloneCa
public PriceList getToPriceList() {
return toPriceList;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseChange{" +
+ "phaseType=" + phaseType +
+ ", fromProduct=" + fromProduct +
+ ", fromProductCategory=" + fromProductCategory +
+ ", fromBillingPeriod=" + fromBillingPeriod +
+ ", fromPriceList=" + fromPriceList +
+ ", toProduct=" + toProduct +
+ ", toProductCategory=" + toProductCategory +
+ ", toBillingPeriod=" + toBillingPeriod +
+ ", toPriceList=" + toPriceList +
+ '}';
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanAlignment.java
index f01860c..d3a95b6 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanAlignment.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanAlignment.java
@@ -68,4 +68,21 @@ public class DefaultCaseChangePlanAlignment extends DefaultCaseChange<PlanAlignm
result = 31 * result + (alignment != null ? alignment.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseChangePlanAlignment {" +
+ "alignment=" + alignment +
+ ", phaseType=" + getPhaseType() +
+ ", fromProduct=" + getFromProduct() +
+ ", fromProductCategory=" + getFromProductCategory() +
+ ", fromBillingPeriod=" + getFromBillingPeriod() +
+ ", fromPriceList=" + getFromPriceList() +
+ ", toProduct=" + getToProduct() +
+ ", toProductCategory=" + getToProductCategory() +
+ ", toBillingPeriod=" + getToBillingPeriod() +
+ ", toPriceList=" + getToPriceList() +
+ '}';
+ }
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanPolicy.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanPolicy.java
index 1464b73..71f90b5 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanPolicy.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseChangePlanPolicy.java
@@ -16,16 +16,16 @@
package org.killbill.billing.catalog.rules;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlSeeAlso;
+import org.killbill.billing.catalog.CatalogSafetyInitializer;
+import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.BillingActionPolicy;
-import org.killbill.billing.catalog.api.BillingPeriod;
-import org.killbill.billing.catalog.api.PhaseType;
-import org.killbill.billing.catalog.api.PriceList;
-import org.killbill.billing.catalog.api.Product;
-import org.killbill.billing.catalog.api.ProductCategory;
import org.killbill.billing.catalog.api.rules.CaseChangePlanPolicy;
+import org.killbill.xmlloader.ValidationErrors;
@XmlSeeAlso(DefaultCaseChange.class)
public class DefaultCaseChangePlanPolicy extends DefaultCaseChange<BillingActionPolicy> implements CaseChangePlanPolicy {
@@ -49,6 +49,17 @@ public class DefaultCaseChangePlanPolicy extends DefaultCaseChange<BillingAction
}
@Override
+ public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
+ return errors;
+ }
+
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+ @Override
public boolean equals(final Object o) {
if (this == o) {
return true;
@@ -75,4 +86,20 @@ public class DefaultCaseChangePlanPolicy extends DefaultCaseChange<BillingAction
result = 31 * result + (policy != null ? policy.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseChangePlanPolicy {" +
+ "policy=" + policy +
+ ", phaseType=" + phaseType +
+ ", fromProduct=" + getFromProduct() +
+ ", fromProductCategory=" + getFromProductCategory() +
+ ", fromBillingPeriod=" + getFromBillingPeriod() +
+ ", fromPriceList=" + getFromPriceList() +
+ ", toProduct=" + getToProduct() +
+ ", toProductCategory=" + getToProductCategory() +
+ ", toBillingPeriod=" + getToBillingPeriod() +
+ ", toPriceList=" + getToPriceList() +
+ '}';
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCreateAlignment.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCreateAlignment.java
index 5d7862d..5577db3 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCreateAlignment.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseCreateAlignment.java
@@ -68,4 +68,16 @@ public class DefaultCaseCreateAlignment extends DefaultCaseStandardNaming<PlanAl
result = 31 * result + (alignment != null ? alignment.hashCode() : 0);
return result;
}
+
+ @Override
+ public String toString() {
+ return "DefaultCaseCreateAlignment {" +
+ "alignment =" + alignment +
+ ", product=" + getProduct() +
+ ", productCategory=" + getProductCategory() +
+ ", billingPeriod=" + getBillingPeriod() +
+ ", priceList=" + getPriceList() +
+ '}';
+ }
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePhase.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePhase.java
index cf656ef..ef8773c 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePhase.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePhase.java
@@ -16,8 +16,11 @@
package org.killbill.billing.catalog.rules;
+import java.net.URI;
+
import javax.xml.bind.annotation.XmlElement;
+import org.killbill.billing.catalog.CatalogSafetyInitializer;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.PhaseType;
@@ -57,6 +60,13 @@ public abstract class DefaultCasePhase<T> extends DefaultCaseStandardNaming<T> {
return errors;
}
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+ }
+
+
public DefaultCasePhase<T> setPhaseType(final PhaseType phaseType) {
this.phaseType = phaseType;
return this;
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePriceList.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePriceList.java
index 7958365..4b9cf33 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePriceList.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCasePriceList.java
@@ -143,4 +143,16 @@ public class DefaultCasePriceList extends DefaultCaseStandardNaming<DefaultPrice
result = 31 * result + (toPriceList != null ? toPriceList.hashCode() : 0);
return result;
}
+
+
+ @Override
+ public String toString() {
+ return "DefaultCasePriceList {" +
+ "fromProduct=" + fromProduct +
+ ", fromProductCategory=" + fromProductCategory +
+ ", fromBillingPeriod=" + fromBillingPeriod +
+ ", fromPriceList=" + fromPriceList +
+ ", toPriceList=" + toPriceList +
+ '}';
+ }
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseStandardNaming.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseStandardNaming.java
index 79c73aa..d23c55f 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseStandardNaming.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultCaseStandardNaming.java
@@ -115,4 +115,5 @@ public abstract class DefaultCaseStandardNaming<T> extends DefaultCase<T> implem
result = 31 * result + (priceList != null ? priceList.hashCode() : 0);
return result;
}
+
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
index 99efa9c..bc7f4ae 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/rules/DefaultPlanRules.java
@@ -16,13 +16,16 @@
package org.killbill.billing.catalog.rules;
+import java.net.URI;
import java.util.Arrays;
+import java.util.HashSet;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
+import org.killbill.billing.catalog.CatalogSafetyInitializer;
import org.killbill.billing.catalog.DefaultPriceList;
import org.killbill.billing.catalog.StandaloneCatalog;
import org.killbill.billing.catalog.api.BillingActionPolicy;
@@ -43,6 +46,7 @@ import org.killbill.billing.catalog.api.rules.CaseCreateAlignment;
import org.killbill.billing.catalog.api.rules.CasePriceList;
import org.killbill.billing.catalog.api.rules.PlanRules;
import org.killbill.xmlloader.ValidatingConfig;
+import org.killbill.xmlloader.ValidationError;
import org.killbill.xmlloader.ValidationErrors;
import com.google.common.collect.ImmutableList;
@@ -155,7 +159,7 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
}
private DefaultPriceList findPriceList(final PlanSpecifier specifier, final StaticCatalog catalog) throws CatalogApiException {
- DefaultPriceList result = DefaultCase.getResult(priceListCase, specifier, catalog);
+ DefaultPriceList result = DefaultCasePriceList.getResult(priceListCase, specifier, catalog);
if (result == null) {
final String priceListName = specifier.getPlanName() != null ? catalog.findCurrentPlan(specifier.getPlanName()).getPriceListName() : specifier.getPriceListName();
result = (DefaultPriceList) catalog.findCurrentPricelist(priceListName);
@@ -163,16 +167,122 @@ public class DefaultPlanRules extends ValidatingConfig<StandaloneCatalog> implem
return result;
}
+
@Override
public ValidationErrors validate(final StandaloneCatalog catalog, final ValidationErrors errors) {
- //TODO: MDW - Validation: check that the plan change special case pairs are unique!
- //TODO: MDW - Validation: check that the each product appears in at most one tier.
- //TODO: MDW - Unit tests for rules
- //TODO: MDW - validate that there is a default policy for change AND cancel
+ //
+ // Validate that there is a default policy for change AND cancel rules and check unicity of rules
+ //
+ final HashSet<DefaultCaseChangePlanPolicy> caseChangePlanPoliciesSet = new HashSet<DefaultCaseChangePlanPolicy>();
+ boolean foundDefaultCase = false;
+ for (final DefaultCaseChangePlanPolicy cur : changeCase) {
+ if (caseChangePlanPoliciesSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for change plan %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ caseChangePlanPoliciesSet.add(cur);
+ }
+ if (cur.getPhaseType() == null &&
+ cur.getFromProduct() == null &&
+ cur.getFromProductCategory() == null &&
+ cur.getFromBillingPeriod() == null &&
+ cur.getFromPriceList() == null &&
+ cur.getToProduct() == null &&
+ cur.getToProductCategory() == null &&
+ cur.getToBillingPeriod() == null &&
+ cur.getToPriceList() == null) {
+ foundDefaultCase = true;
+ }
+ }
+ if (!foundDefaultCase) {
+ errors.add(new ValidationError("Missing default rule case for plan change", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ }
+
+ final HashSet<DefaultCaseCancelPolicy> defaultCaseCancelPoliciesSet = new HashSet<DefaultCaseCancelPolicy>();
+ foundDefaultCase = false;
+ for (final DefaultCaseCancelPolicy cur : cancelCase) {
+ if (defaultCaseCancelPoliciesSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for plan cancellation %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ defaultCaseCancelPoliciesSet.add(cur);
+ }
+ if (cur.getPhaseType() == null &&
+ cur.getProduct() == null &&
+ cur.getProductCategory() == null &&
+ cur.getBillingPeriod() == null &&
+ cur.getPriceList() == null) {
+ foundDefaultCase = true;
+ }
+ }
+ if (!foundDefaultCase) {
+ errors.add(new ValidationError("Missing default rule case for plan cancellation", catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ }
+
+
+ final HashSet<DefaultCaseChangePlanAlignment> caseChangePlanAlignmentsSet = new HashSet<DefaultCaseChangePlanAlignment>();
+ for (final DefaultCaseChangePlanAlignment cur : changeAlignmentCase) {
+ if (caseChangePlanAlignmentsSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for plan change alignment %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ caseChangePlanAlignmentsSet.add(cur);
+ }
+ }
+
+ final HashSet<DefaultCaseCreateAlignment> caseCreateAlignmentsSet = new HashSet<DefaultCaseCreateAlignment>();
+ for (final DefaultCaseCreateAlignment cur : createAlignmentCase) {
+ if (caseCreateAlignmentsSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for create plan alignment %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ caseCreateAlignmentsSet.add(cur);
+ }
+ }
+
+ final HashSet<DefaultCaseBillingAlignment> caseBillingAlignmentsSet = new HashSet<DefaultCaseBillingAlignment>();
+ for (final DefaultCaseBillingAlignment cur : billingAlignmentCase) {
+ if (caseBillingAlignmentsSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for billing alignment %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ caseBillingAlignmentsSet.add(cur);
+ }
+ }
+ final HashSet<DefaultCasePriceList> casePriceListsSet = new HashSet<DefaultCasePriceList>();
+ for (final DefaultCasePriceList cur : priceListCase) {
+ if (casePriceListsSet.contains(cur)) {
+ errors.add(new ValidationError(String.format("Duplicate rule for price list transition %s", cur.toString()), catalog.getCatalogURI(), DefaultPlanRules.class, ""));
+ } else {
+ casePriceListsSet.add(cur);
+ }
+ }
return errors;
}
+
+ @Override
+ public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+
+ for (final DefaultCaseChangePlanPolicy cur : changeCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ for (final DefaultCaseChangePlanAlignment cur : changeAlignmentCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ for (final DefaultCaseCancelPolicy cur : cancelCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ for (final DefaultCaseCreateAlignment cur : createAlignmentCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ for (final DefaultCaseBillingAlignment cur : billingAlignmentCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ for (final DefaultCasePriceList cur : priceListCase) {
+ cur.initialize(catalog, sourceURI);
+ }
+ }
+
+
/////////////////////////////////////////////////////////////////////////////////////
// Setters for testing
/////////////////////////////////////////////////////////////////////////////////////
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
index e6e6cf1..96869e4 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalog.java
@@ -68,7 +68,7 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
@XmlElement(required = true)
private BillingMode recurringBillingMode;
- @XmlElementWrapper(name = "currencies", required = false)
+ @XmlElementWrapper(name = "currencies", required = true)
@XmlElement(name = "currency", required = false)
private Currency[] supportedCurrencies;
@@ -76,14 +76,14 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
@XmlElement(name = "unit", required = false)
private DefaultUnit[] units;
- @XmlElementWrapper(name = "products", required = false)
+ @XmlElementWrapper(name = "products", required = true)
@XmlElement(type=DefaultProduct.class, name = "product", required = false)
private CatalogEntityCollection<Product> products;
@XmlElement(name = "rules", required = true)
private DefaultPlanRules planRules;
- @XmlElementWrapper(name = "plans", required = false)
+ @XmlElementWrapper(name = "plans", required = true)
@XmlElement(type=DefaultPlan.class, name = "plan", required = false)
private CatalogEntityCollection<Plan> plans;
@@ -298,10 +298,16 @@ public class StandaloneCatalog extends ValidatingConfig<StandaloneCatalog> imple
@Override
public void initialize(final StandaloneCatalog catalog, final URI sourceURI) {
- catalogURI = sourceURI;
+
super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
+
+ catalogURI = sourceURI;
planRules.initialize(catalog, sourceURI);
priceLists.initialize(catalog, sourceURI);
+ for (final DefaultUnit cur : units) {
+ cur.initialize(catalog, sourceURI);
+ }
for (final Product p : products.getEntries()) {
((DefaultProduct)p).initialize(catalog, sourceURI);
}
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 8a42e4b..7e6deb9 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -79,7 +79,7 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
}
final InternalCallContext internalCallContext = overrides.getCallContext() != null ? internalCallContextFactory.createInternalCallContextWithoutAccountRecordId(overrides.getCallContext()) : null;
- return priceOverride.getOrCreateOverriddenPlan(defaultPlan, CatalogDateHelper.toUTCDateTime(getEffectiveDate()), overrides.getOverrides(), internalCallContext);
+ return priceOverride.getOrCreateOverriddenPlan(this, defaultPlan, CatalogDateHelper.toUTCDateTime(getEffectiveDate()), overrides.getOverrides(), internalCallContext);
}
@Override
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
index 83f07ac..b8f8fd0 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/VersionedCatalog.java
@@ -26,6 +26,8 @@ import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
import javax.annotation.Nullable;
import javax.xml.bind.annotation.XmlAccessType;
@@ -58,6 +60,7 @@ import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.catalog.api.Unit;
import org.killbill.clock.Clock;
import org.killbill.xmlloader.ValidatingConfig;
+import org.killbill.xmlloader.ValidationError;
import org.killbill.xmlloader.ValidationErrors;
@XmlRootElement(name = "catalogs")
@@ -69,7 +72,11 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
@XmlElementWrapper(name = "versions", required = true)
@XmlElement(name = "version", required = true)
private final List<StandaloneCatalog> versions;
+
+ @XmlElement(required = true)
private String catalogName;
+
+ @XmlElement(required = true)
private BillingMode recurringBillingMode;
// Required for JAXB deserialization
@@ -219,17 +226,9 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
public void add(final StandaloneCatalog e) throws CatalogApiException {
if (catalogName == null) {
catalogName = e.getCatalogName();
- } else {
- if (!catalogName.equals(e.getCatalogName())) {
- throw new CatalogApiException(ErrorCode.CAT_CATALOG_NAME_MISMATCH, catalogName, e.getCatalogName());
- }
}
if (recurringBillingMode == null) {
recurringBillingMode = e.getRecurringBillingMode();
- } else {
- if (!recurringBillingMode.equals(e.getRecurringBillingMode())) {
- throw new CatalogApiException(ErrorCode.CAT_CATALOG_RECURRING_MODE_MISMATCH, recurringBillingMode, e.getRecurringBillingMode());
- }
}
versions.add(e);
Collections.sort(versions, new Comparator<StandaloneCatalog>() {
@@ -400,22 +399,37 @@ public class VersionedCatalog extends ValidatingConfig<VersionedCatalog> impleme
//
@Override
public void initialize(final VersionedCatalog catalog, final URI sourceURI) {
- for (final StandaloneCatalog c : versions) {
- c.initialize(c, sourceURI);
- }
+ //
+ // Initialization is performed first on each StandaloneCatalog (XMLLoader#initializeAndValidate)
+ // and then later on the VersionedCatalog, so we only initialize and validate VersionedCatalog
+ // *without** recursively through each StandaloneCatalog
+ //
+ super.initialize(catalog, sourceURI);
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(this);
}
@Override
public ValidationErrors validate(final VersionedCatalog catalog, final ValidationErrors errors) {
+
+ final Set<Date> effectiveDates = new TreeSet<Date>();
+
for (final StandaloneCatalog c : versions) {
+ if (effectiveDates.contains(c.getEffectiveDate())) {
+ errors.add(new ValidationError(String.format("Catalog effective date '%s' already exists for a previous version", c.getEffectiveDate()),
+ c.getCatalogURI(), VersionedCatalog.class, ""));
+ } else {
+ effectiveDates.add(c.getEffectiveDate());
+ }
+ if (!c.getCatalogName().equals(catalogName)) {
+ errors.add(new ValidationError(String.format("Catalog name '%s' is not consistent across versions ", c.getCatalogName()),
+ c.getCatalogURI(), VersionedCatalog.class, ""));
+ }
+ if (!c.getRecurringBillingMode().equals(recurringBillingMode)) {
+ errors.add(new ValidationError(String.format("Catalog recurringBillingMode '%s' is not consistent across versions ", c.getCatalogName()),
+ c.getCatalogURI(), VersionedCatalog.class, ""));
+ }
errors.addAll(c.validate(c, errors));
}
- //TODO MDW validation - ensure all catalog versions have a single name
- //TODO MDW validation - ensure effective dates are different (actually do we want this?)
- //TODO MDW validation - check that all products are there
- //TODO MDW validation - check that all plans are there
- //TODO MDW validation - check that all currencies are there
- //TODO MDW validation - check that all pricelists are there
return errors;
}
catalog/src/main/resources/EmptyCatalog.xml 10(+10 -0)
diff --git a/catalog/src/main/resources/EmptyCatalog.xml b/catalog/src/main/resources/EmptyCatalog.xml
index c3d9dff..679f928 100644
--- a/catalog/src/main/resources/EmptyCatalog.xml
+++ b/catalog/src/main/resources/EmptyCatalog.xml
@@ -35,6 +35,16 @@
</products>
<rules>
+ <changePolicy>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
</rules>
<plans>
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java b/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java
index 5ba79d3..b93df37 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/MockPlanPhase.java
@@ -28,7 +28,7 @@ public class MockPlanPhase extends DefaultPlanPhase {
public static MockPlanPhase create1USDMonthlyEvergreen() {
return (MockPlanPhase) new MockPlanPhase(BillingPeriod.MONTHLY,
PhaseType.EVERGREEN,
- new DefaultDuration().setUnit(TimeUnit.UNLIMITED),
+ new DefaultDuration().setUnit(TimeUnit.UNLIMITED).setNumber(-1),
MockInternationalPrice.create1USD(),
null).setPlan(MockPlan.createBicycleNoTrialEvergreen1USD());
}
@@ -36,7 +36,7 @@ public class MockPlanPhase extends DefaultPlanPhase {
public static MockPlanPhase createUSDMonthlyEvergreen(final String reccuringUSDPrice, final String fixedPrice) {
return new MockPlanPhase(BillingPeriod.MONTHLY,
PhaseType.EVERGREEN,
- new DefaultDuration().setUnit(TimeUnit.UNLIMITED),
+ new DefaultDuration().setUnit(TimeUnit.UNLIMITED).setNumber(-1),
(reccuringUSDPrice == null) ? null : MockInternationalPrice.createUSD(reccuringUSDPrice),
(fixedPrice == null) ? null : MockInternationalPrice.createUSD(fixedPrice));
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogSafetyInitializer.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogSafetyInitializer.java
new file mode 100644
index 0000000..18989ea
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogSafetyInitializer.java
@@ -0,0 +1,138 @@
+/*
+ * 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.catalog;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlIDREF;
+
+import org.killbill.billing.catalog.api.FixedType;
+import org.killbill.billing.catalog.api.Product;
+import org.killbill.billing.catalog.api.TimeUnit;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class TestCatalogSafetyInitializer {
+
+ @XmlElementWrapper(name = "initialPhasesWrapperAllRequired", required = true)
+ @XmlElement(name = "phase", required = true)
+ private DefaultPlanPhase[] initialPhasesWrapperAllRequired;
+
+ @XmlElementWrapper(name = "initialPhasesWrapperNotRequired", required = false)
+ @XmlElement(name = "phase", required = false)
+ private DefaultPlanPhase[] initialPhasesWrapperNotRequired;
+
+ @XmlElementWrapper(name = "initialPhasesWrapper", required = true)
+ @XmlElement(name = "phase", required = false)
+ private DefaultPlanPhase[] initialPhasesWrapper;
+
+ @XmlElement(name = "pricesNotRequired", required = false)
+ private DefaultPrice[] pricesNotRequired;
+
+ @XmlElement(name = "prices", required = true)
+ private DefaultPrice[] prices;
+
+ @XmlElementWrapper(name = "available", required = false)
+ @XmlIDREF
+ @XmlElement(type = DefaultProduct.class, name = "addonProduct", required = false)
+ private CatalogEntityCollection<Product> available;
+
+ @XmlElement(required = true)
+ private TimeUnit unit;
+
+ @XmlElement(required = false)
+ private Integer number;
+
+ @XmlElement(required = false)
+ private int smallNumber;
+
+ @XmlAttribute(required = false)
+ private FixedType type;
+
+
+
+ @Test(groups = "fast")
+ public void testNonRequiredArrayFields() {
+
+ final TestCatalogSafetyInitializer test = new TestCatalogSafetyInitializer();
+ Assert.assertNull(test.getInitialPhasesWrapperAllRequired());
+ Assert.assertNull(test.getInitialPhasesWrapperNotRequired());
+ Assert.assertNull(test.getInitialPhasesWrapper());
+ Assert.assertNull(test.getPricesNotRequired());
+ Assert.assertNull(test.getPrices());
+
+ CatalogSafetyInitializer.initializeNonRequiredNullFieldsWithDefaultValue(test);
+
+ Assert.assertNull(test.getInitialPhasesWrapperAllRequired());
+ Assert.assertNotNull(test.getInitialPhasesWrapperNotRequired());
+ Assert.assertEquals(test.getInitialPhasesWrapperNotRequired().length, 0);
+ Assert.assertNull(test.getInitialPhasesWrapper());
+ Assert.assertNotNull(test.getPricesNotRequired());
+ Assert.assertEquals(test.getPricesNotRequired().length, 0);
+ Assert.assertNull(test.getPrices());
+
+ Assert.assertNotNull(test.getNumber());
+ Assert.assertEquals(test.getNumber(), CatalogSafetyInitializer.DEFAULT_NON_REQUIRED_INTEGER_FIELD_VALUE);
+
+ Assert.assertNotNull(test.getSmallNumber());
+
+ Assert.assertNotNull(test.getType());
+ Assert.assertEquals(test.getType(), FixedType.ONE_TIME);
+ }
+
+ public DefaultPlanPhase[] getInitialPhasesWrapperAllRequired() {
+ return initialPhasesWrapperAllRequired;
+ }
+
+ public DefaultPlanPhase[] getInitialPhasesWrapperNotRequired() {
+ return initialPhasesWrapperNotRequired;
+ }
+
+ public DefaultPlanPhase[] getInitialPhasesWrapper() {
+ return initialPhasesWrapper;
+ }
+
+ public DefaultPrice[] getPricesNotRequired() {
+ return pricesNotRequired;
+ }
+
+ public DefaultPrice[] getPrices() {
+ return prices;
+ }
+
+ public CatalogEntityCollection<Product> getAvailable() {
+ return available;
+ }
+
+ public TimeUnit getUnit() {
+ return unit;
+ }
+
+ public Integer getNumber() {
+ return number;
+ }
+
+ public int getSmallNumber() {
+ return smallNumber;
+ }
+
+ public FixedType getType() {
+ return type;
+ }
+}
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 a973ad1..279b914 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -205,7 +205,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
assertEquals(plan.getInitialPhases().length, 1);
assertEquals(plan.getInitialPhases()[0].getPhaseType(), PhaseType.TRIAL);
- assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices().length, 3);
+ assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices().length, 0);
assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrice(Currency.EUR), BigDecimal.ZERO);
assertEquals(plan.getInitialPhases()[0].getName(), "standard-monthly-trial");
@@ -279,6 +279,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" <currency>USD</currency>\n" +
" <currency>GBP</currency>\n" +
" </currencies>\n" +
+ " <units/>\n" +
" <products>\n" +
" <product name=\"Dynamic\">\n" +
" <category>BASE</category>\n" +
@@ -384,16 +385,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" <number>30</number>\n" +
" </duration>\n" +
" <fixed type=\"ONE_TIME\">\n" +
- " <fixedPrice>\n" +
- " <price>\n" +
- "<currency>USD</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " <price>\n" +
- "<currency>GBP</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " </fixedPrice>\n" +
+ " <fixedPrice/>\n" +
" </fixed>\n" +
" <usages/>\n" +
" </phase>\n" +
@@ -429,16 +421,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" <number>30</number>\n" +
" </duration>\n" +
" <fixed type=\"ONE_TIME\">\n" +
- " <fixedPrice>\n" +
- " <price>\n" +
- "<currency>USD</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " <price>\n" +
- "<currency>GBP</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " </fixedPrice>\n" +
+ " <fixedPrice/>\n" +
" </fixed>\n" +
" <usages/>\n" +
" </phase>\n" +
@@ -474,16 +457,7 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
" <number>30</number>\n" +
" </duration>\n" +
" <fixed type=\"ONE_TIME\">\n" +
- " <fixedPrice>\n" +
- " <price>\n" +
- "<currency>USD</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " <price>\n" +
- "<currency>GBP</currency>\n" +
- "<value>0</value>\n" +
- " </price>\n" +
- " </fixedPrice>\n" +
+ " <fixedPrice/>\n" +
" </fixed>\n" +
" <usages/>\n" +
" </phase>\n" +
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestDefaultPriceOverride.java b/catalog/src/test/java/org/killbill/billing/catalog/TestDefaultPriceOverride.java
index 3f62d98..75cdd21 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestDefaultPriceOverride.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestDefaultPriceOverride.java
@@ -52,6 +52,7 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
public void testBasic() throws Exception {
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ catalog.initialize(catalog, null);
final Plan plan = catalog.findCurrentPlan("discount-standard-monthly");
final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
@@ -60,7 +61,7 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
final PlanPhasePriceOverride phase3 = new DefaultPlanPhasePriceOverride(plan.getAllPhases()[2].getName(), Currency.USD, null, new BigDecimal("142.41"),null);
overrides.add(phase3);
- final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
+ final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(catalog, plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(overriddenPlan.getName());
assertTrue(m.matches());
@@ -105,13 +106,15 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
public void testWithInvalidPriceOverride() throws Exception {
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ catalog.initialize(catalog, null);
+
final Plan plan = catalog.findCurrentPlan("discount-standard-monthly");
final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
final PlanPhasePriceOverride phase1 = new DefaultPlanPhasePriceOverride(plan.getAllPhases()[0].getName(), Currency.USD, null, BigDecimal.ONE,null);
overrides.add(phase1);
- priceOverride.getOrCreateOverriddenPlan(plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
+ priceOverride.getOrCreateOverriddenPlan(catalog, plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
}
@@ -119,6 +122,8 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
public void testGetOverriddenPlan() throws Exception {
final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ catalog.initialize(catalog, null);
+
final Plan plan = catalog.findCurrentPlan("discount-standard-monthly");
final List<PlanPhasePriceOverride> overrides = new ArrayList<PlanPhasePriceOverride>();
@@ -127,7 +132,7 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
final PlanPhasePriceOverride phase3 = new DefaultPlanPhasePriceOverride(plan.getAllPhases()[2].getName(), Currency.USD, null, new BigDecimal("142.41"),null);
overrides.add(phase3);
- final DefaultPlan overriddenPlanCreated = priceOverride.getOrCreateOverriddenPlan(plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
+ final DefaultPlan overriddenPlanCreated = priceOverride.getOrCreateOverriddenPlan(catalog, plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
System.out.println("overriddenPlanCreated = " + overriddenPlanCreated.getName());
@@ -169,17 +174,25 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
}
}
- private void assertInternationalPrice(final InternationalPrice newInternationalPrice, final InternationalPrice initInternationalPrice, final PlanPhasePriceOverride override, final boolean isFixed) throws CurrencyValueNull {
- assertEquals(newInternationalPrice.getPrices().length, initInternationalPrice.getPrices().length);
- for (int i = 0; i < newInternationalPrice.getPrices().length; i++) {
- final Price initPrice = initInternationalPrice.getPrices()[i];
- final Price newPrice = newInternationalPrice.getPrices()[i];
- if (override != null && override.getCurrency() == initPrice.getCurrency() &&
- ((isFixed && override.getFixedPrice() != null) || (!isFixed && override.getRecurringPrice() != null))) {
- assertEquals(newPrice.getValue().compareTo(isFixed ? override.getFixedPrice() : override.getRecurringPrice()), 0);
- } else {
- if (initPrice != null && initPrice.getValue() != null) {
- assertEquals(newPrice.getValue().compareTo(initPrice.getValue()), 0);
+ private void assertInternationalPrice(final InternationalPrice newInternationalPrice, final InternationalPrice initInternationalPrice, final PlanPhasePriceOverride override, final boolean isFixed) throws CatalogApiException {
+
+ if (initInternationalPrice.getPrices().length == 0) {
+ if (override != null) {
+ assertEquals(newInternationalPrice.getPrices().length, 1);
+ assertEquals(newInternationalPrice.getPrice(override.getCurrency()).compareTo(isFixed ? override.getFixedPrice() : override.getRecurringPrice()), 0);
+ }
+ } else {
+ assertEquals(newInternationalPrice.getPrices().length, initInternationalPrice.getPrices().length);
+ for (int i = 0; i < newInternationalPrice.getPrices().length; i++) {
+ final Price initPrice = initInternationalPrice.getPrices()[i];
+ final Price newPrice = newInternationalPrice.getPrices()[i];
+ if (override != null && override.getCurrency() == initPrice.getCurrency() &&
+ ((isFixed && override.getFixedPrice() != null) || (!isFixed && override.getRecurringPrice() != null))) {
+ assertEquals(newPrice.getValue().compareTo(isFixed ? override.getFixedPrice() : override.getRecurringPrice()), 0);
+ } else {
+ if (initPrice != null && initPrice.getValue() != null) {
+ assertEquals(newPrice.getValue().compareTo(initPrice.getValue()), 0);
+ }
}
}
}
@@ -206,7 +219,7 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
overrides.add(phase);
//Overriding only the tieredblockprice for unit - 'chocolate-videos' with size = 1 and max = 10000 from $1 to $0.75
- final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
+ final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(catalog, plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(overriddenPlan.getName());
@@ -282,7 +295,7 @@ public class TestDefaultPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
/* Overriding phase recurring price from $30 to $35, tieredblockprice from $2 to $1.5 for unit- 'chocolate-videos' with size = 1 and max = 5 and
also overriding tieredblockprice from $1 to $0.75 for unit - 'chocolate-videos' with size = 1 and max = 10000 */
- final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
+ final DefaultPlan overriddenPlan = priceOverride.getOrCreateOverriddenPlan(catalog, plan, new DateTime(catalog.getEffectiveDate()), overrides, internalCallContext);
final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(overriddenPlan.getName());
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java b/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java
index 12cc090..103777a 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestInternationalPrice.java
@@ -34,7 +34,7 @@ public class TestInternationalPrice extends CatalogTestSuiteNoDB {
final StandaloneCatalog c = new MockCatalog();
c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
final DefaultInternationalPrice p0 = new MockInternationalPrice();
- p0.setPrices(null);
+ p0.setPrices(new DefaultPrice[0]);
p0.initialize(c, new URI("foo:bar"));
final DefaultInternationalPrice p1 = new MockInternationalPrice();
p1.setPrices(new DefaultPrice[]{
@@ -63,7 +63,8 @@ public class TestInternationalPrice extends CatalogTestSuiteNoDB {
public void testPriceInitialization() throws URISyntaxException, CatalogApiException {
final StandaloneCatalog c = new MockCatalog();
c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
- ((DefaultInternationalPrice) c.getCurrentPlans().iterator().next().getFinalPhase().getRecurring().getRecurringPrice()).setPrices(null);
+ ((DefaultInternationalPrice) c.getCurrentPlans().iterator().next().getFinalPhase().getRecurring().getRecurringPrice()).setPrices(new DefaultPrice[0]);
+ c.setUnits(new DefaultUnit[0]);
c.initialize(c, new URI("foo://bar"));
Assert.assertEquals(c.getCurrentPlans().iterator().next().getFinalPhase().getRecurring().getRecurringPrice().getPrice(Currency.GBP), new BigDecimal(0));
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java b/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java
index 86f5033..8e466cf 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestPlan.java
@@ -32,9 +32,10 @@ public class TestPlan extends CatalogTestSuiteNoDB {
final StandaloneCatalog c = new MockCatalog();
c.setSupportedCurrencies(new Currency[]{Currency.GBP, Currency.EUR, Currency.USD, Currency.BRL, Currency.MXN});
final DefaultPlan p1 = MockPlan.createBicycleTrialEvergreen1USD();
+ p1.setPlansAllowedInBundle(-1);
p1.setEffectiveDateForExistingSubscriptions(new Date((new Date().getTime()) - (1000 * 60 * 60 * 24)));
final ValidationErrors errors = p1.validate(c, new ValidationErrors());
- Assert.assertEquals(errors.size(), 1);
+ Assert.assertEquals(errors.size(), 3);
errors.log(log);
}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
index 78b4554..c7c411a 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalog.java
@@ -16,18 +16,44 @@
package org.killbill.billing.catalog;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.catalog.api.Plan;
+import org.killbill.xmlloader.ValidationException;
+import org.killbill.xmlloader.XMLLoader;
import org.testng.Assert;
import org.testng.annotations.Test;
-import org.killbill.billing.catalog.api.CatalogApiException;
-import org.killbill.billing.catalog.api.PhaseType;
-
import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
public class TestStandaloneCatalog extends CatalogTestSuiteNoDB {
@Test(groups = "fast")
+ public void testLoadCatalogWithValidationIssues() throws Exception {
+ try {
+ XMLLoader.getObjectFromString(Resources.getResource("CatalogWithValidationErrors.xml").toExternalForm(), StandaloneCatalog.class);
+ Assert.fail();
+ } catch (final ValidationException e) {
+ Assert.assertEquals(e.getErrors().size(), 13);
+ Assert.assertEquals(e.getErrors().get(0).getDescription(), "Invalid product for plan 'standard'");
+ Assert.assertEquals(e.getErrors().get(1).getDescription(), "Duration can only have 'UNLIMITED' unit if the number is omitted");
+ Assert.assertEquals(e.getErrors().get(2).getDescription(), "Finite Duration must have a well defined length");
+ Assert.assertEquals(e.getErrors().get(3).getDescription(), "Initial Phase standard-trial-evergreen of plan standard-trial cannot be of type EVERGREEN");
+ Assert.assertEquals(e.getErrors().get(4).getDescription(), "Final Phase standard-trial-trial of plan standard-trial cannot be of type TRIAL");
+ Assert.assertEquals(e.getErrors().get(5).getDescription(), "Duplicate rule for change plan DefaultCaseChangePlanPolicy {policy=IMMEDIATE, phaseType=null, fromProduct=DefaultProduct{name='Standard', category=BASE, included=org.killbill.billing.catalog.CatalogEntityCollection@0, available=org.killbill.billing.catalog.CatalogEntityCollection@0, limits=[], catalogName='CatalogWithValidationErrors'}, fromProductCategory=null, fromBillingPeriod=null, fromPriceList=null, toProduct=null, toProductCategory=null, toBillingPeriod=null, toPriceList=null}");
+ Assert.assertEquals(e.getErrors().get(6).getDescription(), "Missing default rule case for plan change");
+ Assert.assertEquals(e.getErrors().get(7).getDescription(), "Duplicate rule for plan cancellation DefaultCaseCancelPolicy{policy =IMMEDIATE, phaseType =null, product=DefaultProduct{name='Standard', category=BASE, included=org.killbill.billing.catalog.CatalogEntityCollection@0, available=org.killbill.billing.catalog.CatalogEntityCollection@0, limits=[], catalogName='CatalogWithValidationErrors'}, productCategory=null, billingPeriod=null, priceList=null}");
+ Assert.assertEquals(e.getErrors().get(8).getDescription(), "Missing default rule case for plan cancellation");
+ Assert.assertEquals(e.getErrors().get(9).getDescription(), "Duplicate rule for plan change alignment DefaultCaseChangePlanAlignment {alignment=START_OF_BUNDLE, phaseType=null, fromProduct=null, fromProductCategory=null, fromBillingPeriod=null, fromPriceList=null, toProduct=null, toProductCategory=null, toBillingPeriod=null, toPriceList=null}");
+ Assert.assertEquals(e.getErrors().get(10).getDescription(), "Duplicate rule for create plan alignment DefaultCaseCreateAlignment {alignment =START_OF_BUNDLE, product=null, productCategory=null, billingPeriod=null, priceList=null}");
+ Assert.assertEquals(e.getErrors().get(11).getDescription(), "Duplicate rule for billing alignment DefaultCaseBillingAlignment {alignment=ACCOUNT, phaseType=null, product=null, productCategory=null, billingPeriod=null, priceList=null}");
+ Assert.assertEquals(e.getErrors().get(12).getDescription(), "Duplicate rule for price list transition DefaultCasePriceList {fromProduct=null, fromProductCategory=null, fromBillingPeriod=null, fromPriceList=null, toPriceList=DefaultPriceList{name='DEFAULT}}");
+
+ }
+ }
+
+ @Test(groups = "fast")
public void testFindPhase() throws CatalogApiException {
final DefaultPlanPhase phaseTrial1 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
final DefaultPlanPhase phaseTrial2 = new MockPlanPhase().setPhaseType(PhaseType.TRIAL);
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
new file mode 100644
index 0000000..2a70ca6
--- /dev/null
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
@@ -0,0 +1,66 @@
+/*
+ * 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.catalog;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.xmlloader.XMLLoader;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.io.Resources;
+
+public class TestStandaloneCatalogWithPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
+
+ @Test(groups = "slow")
+ public void testCreatePlanNoProduct() throws Exception {
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ try {
+ final PlanSpecifier specWithNullProduct = new PlanSpecifier(null, BillingPeriod.ANNUAL, "DEFAULT");
+ standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(specWithNullProduct, null);
+ Assert.fail();
+ } catch (final CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NULL_PRODUCT_NAME.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
+ public void testCreatePlanInvalidProduct() throws Exception {
+ final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+ final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+ priceOverride,
+ internalCallContext.getTenantRecordId(),
+ internalCallContextFactory);
+
+ try {
+ final PlanSpecifier specWithNullProduct = new PlanSpecifier("INVALID", BillingPeriod.ANNUAL, "DEFAULT");
+ standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(specWithNullProduct, null);
+ Assert.fail();
+ } catch (final CatalogApiException e) {
+ Assert.assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PRODUCT.getCode());
+ }
+ }
+}
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index 670abb5..8f28d1c 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -119,10 +119,6 @@
<policy>IMMEDIATE</policy>
</changePolicyCase>
<changePolicyCase>
- <toPriceList>rescue</toPriceList>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
<fromBillingPeriod>MONTHLY</fromBillingPeriod>
<toBillingPeriod>ANNUAL</toBillingPeriod>
<policy>IMMEDIATE</policy>
diff --git a/catalog/src/test/resources/CatalogWithValidationErrors.xml b/catalog/src/test/resources/CatalogWithValidationErrors.xml
new file mode 100644
index 0000000..26758f8
--- /dev/null
+++ b/catalog/src/test/resources/CatalogWithValidationErrors.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ 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.
+ -->
+
+<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="CatalogSchema.xsd ">
+
+ <effectiveDate>2013-02-08T00:00:00+00:00</effectiveDate>
+ <catalogName>CatalogWithValidationErrors</catalogName>
+
+ <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
+
+ <currencies>
+ <currency>USD</currency>
+ </currencies>
+
+ <products>
+ <product name="Standard">
+ <category>BASE</category>
+ </product>
+ </products>
+
+
+
+ <!-- Note we defined twice the same rule for each case and also we miss default rules for plan cancellation and change of plan -->
+ <rules>
+ <changePolicy>
+ <changePolicyCase>
+ <fromProduct>Standard</fromProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ <changePolicyCase>
+ <fromProduct>Standard</fromProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <changeAlignment>
+ <changeAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </changeAlignmentCase>
+ <changeAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </changeAlignmentCase>
+ </changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <product>Standard</product>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ <cancelPolicyCase>
+ <product>Standard</product>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
+ <createAlignment>
+ <createAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </createAlignmentCase>
+ <createAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </createAlignmentCase>
+ </createAlignment>
+ <billingAlignment>
+ <billingAlignmentCase>
+ <alignment>ACCOUNT</alignment>
+ </billingAlignmentCase>
+ <billingAlignmentCase>
+ <alignment>ACCOUNT</alignment>
+ </billingAlignmentCase>
+ </billingAlignment>
+ <priceList>
+ <priceListCase>
+ <toPriceList>DEFAULT</toPriceList>
+ </priceListCase>
+ <priceListCase>
+ <toPriceList>DEFAULT</toPriceList>
+ </priceListCase>
+ </priceList>
+ </rules>
+
+
+ <plans>
+ <plan name="standard">
+ <!-- Note the typo: the product name matches the plan name, so there is an XML ID - but it doesn't match the product name -->
+ <product>standard</product>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ <!-- Note UNLIMITED should only support number -1 or undefined -->
+ <number>1</number>
+ </duration>
+ <recurring>
+ <billingPeriod>NO_BILLING_PERIOD</billingPeriod>
+ </recurring>
+ </finalPhase>
+ </plan>
+ <plan name="standard-trial">
+ <product>Standard</product>
+ <initialPhases>
+ <!-- Note we should not have an EVERGREEN phase to start -->
+ <phase type="EVERGREEN">
+ <duration>
+ <unit>DAYS</unit>
+ <!-- Note we did not specify the number -->
+ </duration>
+ <fixed>
+ <fixedPrice> <!-- empty price implies $0 -->
+ </fixedPrice>
+
+ </fixed>
+ </phase>
+ </initialPhases>
+ <!-- Note we should not have an TRIAL phase to finish -->
+ <finalPhase type="TRIAL">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>NO_BILLING_PERIOD</billingPeriod>
+ </recurring>
+ </finalPhase>
+ </plan>
+ </plans>
+
+
+
+
+
+ <priceLists>
+ <defaultPriceList name="DEFAULT">
+ <plans>
+ <plan>standard</plan>
+ <plan>standard-trial</plan>
+ </plans>
+ </defaultPriceList>
+ </priceLists>
+</catalog>
diff --git a/catalog/src/test/resources/UsageExperimental.xml b/catalog/src/test/resources/UsageExperimental.xml
index 9fd614f..02d2a50 100644
--- a/catalog/src/test/resources/UsageExperimental.xml
+++ b/catalog/src/test/resources/UsageExperimental.xml
@@ -66,10 +66,19 @@
</products>
<rules>
+ <changePolicy>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
</rules>
<plans>
-
<plan name="capacity-in-advance-monthly">
<product>CapacityInAdvance</product>
<finalPhase type="EVERGREEN">
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
index b2b9276..f333572 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-1.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>END_OF_TERM</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
index f04ff0e..df1c09d 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-2.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
diff --git a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
index dc9c0b1..eb21229 100644
--- a/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
+++ b/catalog/src/test/resources/versionedCatalog/WeaponsHireSmall-3.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
diff --git a/catalog/src/test/resources/WeaponsHireSmall.xml b/catalog/src/test/resources/WeaponsHireSmall.xml
index 5adfb99..f64b5c6 100644
--- a/catalog/src/test/resources/WeaponsHireSmall.xml
+++ b/catalog/src/test/resources/WeaponsHireSmall.xml
@@ -68,12 +68,20 @@
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
<alignment>START_OF_SUBSCRIPTION</alignment>
</changeAlignmentCase>
</changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
<createAlignment>
<createAlignmentCase>
<product>Laser-Scope</product>
currency/pom.xml 2(+1 -1)
diff --git a/currency/pom.xml b/currency/pom.xml
index 3278f26..d7c5a18 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-currency</artifactId>
entitlement/pom.xml 2(+1 -1)
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index 20c9c9b..1eef933 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-entitlement</artifactId>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
index 429eafe..178af0a 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/BlockingStateOrdering.java
@@ -31,7 +31,6 @@ import java.util.UUID;
import javax.annotation.Nullable;
-import org.joda.time.LocalDate;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.Plan;
@@ -64,8 +63,6 @@ public class BlockingStateOrdering extends EntitlementOrderingBase {
}
private void computeEvents(final Iterable<Entitlement> entitlements, final InternalTenantContext internalTenantContext, final LinkedList<SubscriptionEvent> inputAndOutputResult) {
-
-
final Collection<UUID> allEntitlementUUIDs = new HashSet<UUID>();
final Collection<BlockingState> blockingStates = new LinkedList<BlockingState>();
for (final Entitlement entitlement : entitlements) {
@@ -245,6 +242,13 @@ public class BlockingStateOrdering extends EntitlementOrderingBase {
nextPriceList = prev.getNextPriceList();
nextBillingPeriod = prev.getNextBillingPeriod();
}
+ } else if (prev != null && (SubscriptionEventType.START_ENTITLEMENT.equals(eventType) || SubscriptionEventType.START_BILLING.equals(eventType))) {
+ // For start events, next is actually the prev (e.g. the trial, not the phase)
+ nextProduct = prev.getNextProduct();
+ nextPlan = prev.getNextPlan();
+ nextPlanPhase = prev.getNextPhase();
+ nextPriceList = prev.getNextPriceList();
+ nextBillingPeriod = prev.getNextBillingPeriod();
} else {
nextProduct = next.getNextProduct();
nextPlan = next.getNextPlan();
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index a84469f..88f580a 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -161,11 +161,13 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.createBundleForAccount(accountId, externalKey, contextWithValidAccountRecordId);
- final DateTime billingRequestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getBillingEffectiveDate(), contextWithValidAccountRecordId);
- final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+ final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+ final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(baseEntitlementWithAddOnsSpecifier);
+
+ final DateTime billingRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(), contextWithValidAccountRecordId);
final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundle.getId(), specifier.getPlanPhaseSpecifier(), specifier.getOverrides(), billingRequestedDate, isMigrated, contextWithValidAccountRecordId);
- final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getEntitlementEffectiveDate(), contextWithValidAccountRecordId);
+ final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), contextWithValidAccountRecordId);
final BlockingState newBlockingState = new DefaultBlockingState(subscription.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_START, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, entitlementRequestedDate);
entitlementUtils.setBlockingStatesAndPostBlockingTransitionEvent(ImmutableList.<BlockingState>of(newBlockingState), subscription.getBundleId(), contextWithValidAccountRecordId);
@@ -180,7 +182,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
return pluginExecution.executeWithPlugin(createBaseEntitlementWithPlugin, pluginContext);
}
- private EntitlementSpecifier getFirstEntitlementSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
+ private BaseEntitlementWithAddOnsSpecifier getFirstBaseEntitlementWithAddOnsSpecifier(final Iterable<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifiers) throws SubscriptionBaseApiException {
if (baseEntitlementWithAddOnsSpecifiers == null) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
}
@@ -190,7 +192,10 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
}
- final BaseEntitlementWithAddOnsSpecifier entitlementWithAddOnsSpecifier = iterator.next();
+ return iterator.next();
+ }
+
+ private EntitlementSpecifier getFirstEntitlementSpecifier(final BaseEntitlementWithAddOnsSpecifier entitlementWithAddOnsSpecifier) throws SubscriptionBaseApiException {
if (entitlementWithAddOnsSpecifier.getEntitlementSpecifier() == null || !entitlementWithAddOnsSpecifier.getEntitlementSpecifier().iterator().hasNext()) {
throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_INVALID_ENTITLEMENT_SPECIFIER);
} else {
@@ -297,13 +302,15 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
throw new EntitlementApiException(new BlockingApiException(ErrorCode.BLOCK_BLOCKED_ACTION, BlockingChecker.ACTION_CHANGE, BlockingChecker.TYPE_SUBSCRIPTION, eventsStreamForBaseSubscription.getEntitlementId().toString()));
}
- final DateTime billingRequestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getBillingEffectiveDate(), eventsStreamForBaseSubscription.getInternalTenantContext());
try {
+ final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+ final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(baseEntitlementWithAddOnsSpecifier);
+
final InternalCallContext context = internalCallContextFactory.createInternalCallContext(eventsStreamForBaseSubscription.getAccountId(), callContext);
- final EntitlementSpecifier specifier = getFirstEntitlementSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+ final DateTime billingRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(), context);
final SubscriptionBase subscription = subscriptionBaseInternalApi.createSubscription(bundleId, specifier.getPlanPhaseSpecifier(), specifier.getOverrides(), billingRequestedDate, isMigrated, context);
- final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getEntitlementEffectiveDate(), eventsStreamForBaseSubscription.getInternalTenantContext());
+ final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), context);
final BlockingState newBlockingState = new DefaultBlockingState(subscription.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_START, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, entitlementRequestedDate);
entitlementUtils.setBlockingStatesAndPostBlockingTransitionEvent(ImmutableList.<BlockingState>of(newBlockingState), subscription.getBundleId(), context);
@@ -464,7 +471,9 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
throw new EntitlementApiException(new SubscriptionBaseApiException(ErrorCode.SUB_GET_INVALID_BUNDLE_KEY, externalKey));
}
- final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getBillingEffectiveDate(), contextWithSourceAccountRecordId);
+ final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = getFirstBaseEntitlementWithAddOnsSpecifier(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers());
+
+ final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getBillingEffectiveDate(), contextWithSourceAccountRecordId);
final SubscriptionBaseBundle newBundle = subscriptionBaseTransferApi.transferBundle(sourceAccountId, destAccountId, externalKey, requestedDate, true, cancelImm, context);
@@ -482,7 +491,7 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements
final InternalCallContext contextWithDestAccountRecordId = internalCallContextFactory.createInternalCallContext(destAccountId, context);
blockingStates.clear();
- final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getBaseEntitlementWithAddOnsSpecifiers().iterator().next().getEntitlementEffectiveDate(), contextWithDestAccountRecordId);
+ final DateTime entitlementRequestedDate = dateHelper.fromLocalDateAndReferenceTime(baseEntitlementWithAddOnsSpecifier.getEntitlementEffectiveDate(), contextWithDestAccountRecordId);
for (final SubscriptionBase subscriptionBase : subscriptionBaseInternalApi.getSubscriptionsForBundle(newBundle.getId(), null, contextWithDestAccountRecordId)) {
final BlockingState newBlockingState = new DefaultBlockingState(subscriptionBase.getId(), BlockingStateType.SUBSCRIPTION, DefaultEntitlementApi.ENT_STATE_START, EntitlementService.ENTITLEMENT_SERVICE_NAME, false, false, false, entitlementRequestedDate);
blockingStates.put(newBlockingState, subscriptionBase.getBundleId());
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
index c868eff..eb2a68d 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementContext.java
@@ -1,6 +1,6 @@
/*
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * 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
@@ -17,7 +17,6 @@
package org.killbill.billing.entitlement.api;
-import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -32,6 +31,7 @@ import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.CallOrigin;
import org.killbill.billing.util.callcontext.UserType;
+import com.google.common.collect.Iterables;
public class DefaultEntitlementContext implements EntitlementContext {
@@ -51,18 +51,27 @@ public class DefaultEntitlementContext implements EntitlementContext {
private final DateTime updatedDate;
private final UUID tenantId;
-
public DefaultEntitlementContext(final EntitlementContext prev,
@Nullable final PriorEntitlementResult pluginResult) {
this(prev.getOperationType(),
prev.getAccountId(),
prev.getDestinationAccountId(),
- pluginResult != null && pluginResult.getAdjustedBaseEntitlementWithAddOnsSpecifiers() != null ? pluginResult.getAdjustedBaseEntitlementWithAddOnsSpecifiers() : prev.getBaseEntitlementWithAddOnsSpecifiers(),
+ pluginResult != null ? merge(prev.getBaseEntitlementWithAddOnsSpecifiers(), pluginResult.getAdjustedBaseEntitlementWithAddOnsSpecifiers()) : prev.getBaseEntitlementWithAddOnsSpecifiers(),
pluginResult != null && pluginResult.getAdjustedBillingActionPolicy() != null ? pluginResult.getAdjustedBillingActionPolicy() : prev.getBillingActionPolicy(),
- pluginResult != null && pluginResult.getAdjustedPluginProperties() != null ? pluginResult.getAdjustedPluginProperties() : prev.getPluginProperties(),
+ pluginResult != null ? merge(prev.getPluginProperties(), pluginResult.getAdjustedPluginProperties()) : prev.getPluginProperties(),
prev);
}
+ private static <T> Iterable<T> merge(final Iterable<T> prevValues, final Iterable<T> newValues) {
+ // Be lenient if a plugin returns an empty list (default behavior for Ruby plugins): at this point,
+ // we know the isAborted flag hasn't been set, so let's assume the user actually wants to use the previous list
+ if (newValues != null && !Iterables.<BaseEntitlementWithAddOnsSpecifier>isEmpty(newValues)) {
+ return newValues;
+ } else {
+ return prevValues;
+ }
+ }
+
public DefaultEntitlementContext(final OperationType operationType,
final UUID accountId,
final UUID destinationAccountId,
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
index 82d9278..0596c73 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/EntitlementPluginExecution.java
@@ -120,7 +120,7 @@ public class EntitlementPluginExecution {
continue;
}
prevResult = plugin.priorCall(currentContext, currentContext.getPluginProperties());
- if (prevResult.isAborted()) {
+ if (prevResult != null && prevResult.isAborted()) {
break;
}
currentContext = new DefaultEntitlementContext(currentContext, prevResult);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java b/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
index 8f47820..0793d6f 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/logging/EntitlementLoggingHelper.java
@@ -42,12 +42,12 @@ public abstract class EntitlementLoggingHelper {
final LocalDate billingDate) {
if (log.isInfoEnabled()) {
- final StringBuilder logLine = new StringBuilder("Create ")
+ final StringBuilder logLine = new StringBuilder("Create")
.append(bundleId != null ? " AO " : " BP ")
.append("Entitlement: ");
if (bundleId != null) {
- logLine.append(", bundleId='")
+ logLine.append("bundleId='")
.append(bundleId)
.append("'");
}
diff --git a/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql b/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql
index 8d2d2be..c085a6b 100644
--- a/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql
+++ b/entitlement/src/main/resources/org/killbill/billing/entitlement/ddl.sql
@@ -22,4 +22,5 @@ CREATE TABLE blocking_states (
PRIMARY KEY(record_id)
) /*! CHARACTER SET utf8 COLLATE utf8_bin */;
CREATE INDEX blocking_states_id ON blocking_states(blockable_id);
+CREATE INDEX blocking_states_id_real ON blocking_states(id);
CREATE INDEX blocking_states_tenant_account_record_id ON blocking_states(tenant_record_id, account_record_id);
diff --git a/entitlement/src/main/resources/org/killbill/billing/entitlement/migration/V20161208173547__blocking_states_id_idx.sql b/entitlement/src/main/resources/org/killbill/billing/entitlement/migration/V20161208173547__blocking_states_id_idx.sql
new file mode 100644
index 0000000..f86f355
--- /dev/null
+++ b/entitlement/src/main/resources/org/killbill/billing/entitlement/migration/V20161208173547__blocking_states_id_idx.sql
@@ -0,0 +1 @@
+CREATE INDEX blocking_states_id_real ON blocking_states(id);
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
index 979dde2..be7245c 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -387,6 +387,41 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
final Subscription subscription = subscriptionApi.getSubscriptionForEntitlementId(baseEntitlement.getId(), callContext);
Assert.assertEquals(subscription.getSubscriptionEvents().size(), 7);
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(0).getServiceName(), "entitlement-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(0).getServiceStateName(), "ENT_STARTED");
+ Assert.assertNull(subscription.getSubscriptionEvents().get(0).getPrevPhase());
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(0).getNextPhase().getName(), "shotgun-annual-trial");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(1).getServiceName(), "billing-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(1).getServiceStateName(), "START_BILLING");
+ Assert.assertNull(subscription.getSubscriptionEvents().get(1).getPrevPhase());
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(1).getNextPhase().getName(), "shotgun-annual-trial");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(2).getServiceName(), "entitlement+billing-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(2).getServiceStateName(), "PHASE");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(2).getPrevPhase().getName(), "shotgun-annual-trial");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(2).getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(3).getServiceName(), "entitlement-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(3).getServiceStateName(), "ENT_BLOCKED");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(3).getPrevPhase().getName(), "shotgun-annual-evergreen");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(3).getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(4).getServiceName(), "billing-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(4).getServiceStateName(), "ENT_BLOCKED");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(4).getPrevPhase().getName(), "shotgun-annual-evergreen");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(4).getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(5).getServiceName(), "entitlement-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(5).getServiceStateName(), "ENT_CLEAR");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(5).getPrevPhase().getName(), "shotgun-annual-evergreen");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(5).getNextPhase().getName(), "shotgun-annual-evergreen");
+
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(6).getServiceName(), "billing-service");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(6).getServiceStateName(), "ENT_CLEAR");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(6).getPrevPhase().getName(), "shotgun-annual-evergreen");
+ Assert.assertEquals(subscription.getSubscriptionEvents().get(6).getNextPhase().getName(), "shotgun-annual-evergreen");
+
testListener.pushExpectedEvents(NextEvent.BLOCK);
clock.setDay(pauseDate);
assertListenerStatus();
invoice/pom.xml 2(+1 -1)
diff --git a/invoice/pom.xml b/invoice/pom.xml
index 24b2082..7079bc7 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-invoice</artifactId>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index d7eb9fd..90f868b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -100,7 +100,6 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
dao.notifyOfPaymentInit(new InvoicePaymentModelDao(invoicePayment), context);
}
-
@Override
public void recordPaymentAttemptCompletion(final UUID invoiceId, final BigDecimal amount, final Currency currency, final Currency processedCurrency, final UUID paymentId, final String transactionExternalKey, final DateTime paymentDate, final boolean success, final InternalCallContext context) throws InvoiceApiException {
final InvoicePayment invoicePayment = new DefaultInvoicePayment(InvoicePaymentType.ATTEMPT, paymentId, invoiceId, paymentDate, amount, currency, processedCurrency, transactionExternalKey, success);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 908e709..0baf924 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -270,6 +270,9 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public Iterable<Invoice> prepareInvoices() throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
+ final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());
+
// Group all new external charges on the same invoice (per currency)
final Map<Currency, Invoice> newInvoicesForExternalCharges = new HashMap<Currency, Invoice>();
final Map<UUID, Invoice> existingInvoicesForExternalCharges = new HashMap<UUID, Invoice>();
@@ -282,7 +285,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
final Currency currency = charge.getCurrency();
if (newInvoicesForExternalCharges.get(currency) == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
- final Invoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency, status);
+ final Invoice newInvoiceForExternalCharge = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
newInvoicesForExternalCharges.put(currency, newInvoiceForExternalCharge);
}
invoiceForExternalCharge = newInvoicesForExternalCharges.get(currency);
@@ -348,11 +351,14 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public List<Invoice> prepareInvoices() throws InvoiceApiException {
+ final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(accountId, context);
+ final LocalDate invoiceDate = internalTenantContext.toLocalDate(context.getCreatedDate());
+
// Create an invoice for that credit if it doesn't exist
final Invoice invoiceForCredit;
if (invoiceId == null) {
final InvoiceStatus status = autoCommit ? InvoiceStatus.COMMITTED : InvoiceStatus.DRAFT;
- invoiceForCredit = new DefaultInvoice(accountId, effectiveDate, effectiveDate, currency, status);
+ invoiceForCredit = new DefaultInvoice(accountId, invoiceDate, effectiveDate, currency, status);
} else {
invoiceForCredit = getInvoiceAndCheckCurrency(invoiceId, currency, context);
if (InvoiceStatus.COMMITTED.equals(invoiceForCredit.getStatus())) {
@@ -468,9 +474,8 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
@Override
public UUID createMigrationInvoice(final UUID accountId, final LocalDate targetDate, final Iterable<InvoiceItem> items, final CallContext context) {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(accountId, context);
- final LocalDate createdDate = internalCallContext.toLocalDate(internalCallContext.getCreatedDate());
- final InvoiceModelDao migrationInvoice = new InvoiceModelDao(accountId, createdDate, targetDate, items.iterator().next().getCurrency(), true);
-
+ final LocalDate invoiceDate = internalCallContext.toLocalDate(internalCallContext.getCreatedDate());
+ final InvoiceModelDao migrationInvoice = new InvoiceModelDao(accountId, invoiceDate, targetDate, items.iterator().next().getCurrency(), true);
final List<InvoiceItemModelDao> itemModelDaos = ImmutableList.copyOf(Iterables.transform(items, new Function<InvoiceItem, InvoiceItemModelDao>() {
@Override
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
index 31acf55..c7af0ef 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/calculator/InvoiceCalculatorUtils.java
@@ -218,7 +218,7 @@ public abstract class InvoiceCalculatorUtils {
}
for (final InvoicePayment invoicePayment : invoicePayments) {
- if (!invoicePayment.isSuccess()) {
+ if (invoicePayment.isSuccess() == null || !invoicePayment.isSuccess()) {
continue;
}
if (InvoicePaymentType.REFUND.equals(invoicePayment.getType()) ||
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 acb4dd0..d519cc0 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
@@ -53,6 +53,20 @@ public class MultiTenantInvoiceConfig extends MultiTenantConfigBase implements I
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return staticConfig.isSanitySafetyBoundEnabled();
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ final String result = getStringTenantConfig("isSanitySafetyBoundEnabled", tenantContext);
+ if (result != null) {
+ return Boolean.parseBoolean(result);
+ }
+ return isSanitySafetyBoundEnabled();
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return staticConfig.getMaxDailyNumberOfItemsSafetyBound();
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
index 204a0e5..c452e53 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/DefaultInvoiceDao.java
@@ -732,13 +732,12 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
notifyOfPaymentCompletionInternal(invoicePayment, false, context);
}
-
@Override
public void notifyOfPaymentCompletion(final InvoicePaymentModelDao invoicePayment, final InternalCallContext context) {
notifyOfPaymentCompletionInternal(invoicePayment, true, context);
}
- public void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
+ private void notifyOfPaymentCompletionInternal(final InvoicePaymentModelDao invoicePayment, final boolean completion, final InternalCallContext context) {
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
@@ -764,7 +763,7 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
if (existingAttempt == null) {
transactional.create(invoicePayment, context);
- } else if (!existingAttempt.getSuccess()) {
+ } else {
transactional.updateAttempt(existingAttempt.getRecordId(),
invoicePayment.getPaymentId().toString(),
invoicePayment.getPaymentDate().toDate(),
@@ -1120,34 +1119,41 @@ public class DefaultInvoiceDao extends EntityDaoBase<InvoiceModelDao, Invoice, I
// create child and parent invoices
- final DateTime effectiveDate = childAccountContext.getCreatedDate();
+ final DateTime childCreatedDate = childAccountContext.getCreatedDate();
final BigDecimal accountCBA = getAccountCBA(childAccount.getId(), childAccountContext);
// create external charge to child account
- final Invoice invoiceForExternalCharge = new DefaultInvoice(childAccount.getId(), effectiveDate.toLocalDate(),
- effectiveDate.toLocalDate(),
- childAccount.getCurrency(), InvoiceStatus.COMMITTED);
+ final LocalDate childInvoiceDate = childAccountContext.toLocalDate(childAccountContext.getCreatedDate());
+ final Invoice invoiceForExternalCharge = new DefaultInvoice(childAccount.getId(),
+ childInvoiceDate,
+ childCreatedDate.toLocalDate(),
+ childAccount.getCurrency(),
+ InvoiceStatus.COMMITTED);
final String chargeDescription = "Charge to move credit from child to parent account";
final InvoiceItem externalChargeItem = new ExternalChargeInvoiceItem(UUIDs.randomUUID(),
- effectiveDate,
+ childCreatedDate,
invoiceForExternalCharge.getId(),
childAccount.getId(),
null,
chargeDescription,
- effectiveDate.toLocalDate(),
+ childCreatedDate.toLocalDate(),
accountCBA,
childAccount.getCurrency());
invoiceForExternalCharge.addInvoiceItem(externalChargeItem);
// create credit to parent account
- final Invoice invoiceForCredit = new DefaultInvoice(childAccount.getParentAccountId(), effectiveDate.toLocalDate(), effectiveDate.toLocalDate(),
- childAccount.getCurrency(), InvoiceStatus.COMMITTED);
+ final LocalDate parentInvoiceDate = parentAccountContext.toLocalDate(parentAccountContext.getCreatedDate());
+ final Invoice invoiceForCredit = new DefaultInvoice(childAccount.getParentAccountId(),
+ parentInvoiceDate,
+ childCreatedDate.toLocalDate(),
+ childAccount.getCurrency(),
+ InvoiceStatus.COMMITTED);
final String creditDescription = "Credit migrated from child account " + childAccount.getId();
final InvoiceItem creditItem = new CreditAdjInvoiceItem(UUIDs.randomUUID(),
- effectiveDate,
+ childCreatedDate,
invoiceForCredit.getId(),
childAccount.getParentAccountId(),
- effectiveDate.toLocalDate(),
+ childCreatedDate.toLocalDate(),
creditDescription,
// Note! The amount is negated here!
accountCBA.negate(),
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
index 589d1c7..8043b07 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/dao/InvoiceModelDao.java
@@ -78,10 +78,6 @@ public class InvoiceModelDao extends EntityModelDaoBase implements EntityModelDa
this(UUIDs.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, migrated, status, false);
}
- public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final LocalDate targetDate, final Currency currency) {
- this(UUIDs.randomUUID(), null, accountId, null, invoiceDate, targetDate, currency, false, InvoiceStatus.COMMITTED, false);
- }
-
public InvoiceModelDao(final UUID accountId, final LocalDate invoiceDate, final Currency currency, final InvoiceStatus status, final boolean isParentInvoice) {
this(UUIDs.randomUUID(), invoiceDate.toDateTimeAtCurrentTime(), accountId, null, invoiceDate, null, currency, false, status, isParentInvoice);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
index f35087b..1e64bb6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/DefaultInvoiceGenerator.java
@@ -76,7 +76,7 @@ public class DefaultInvoiceGenerator implements InvoiceGenerator {
validateTargetDate(targetDate, context);
final LocalDate adjustedTargetDate = adjustTargetDate(existingInvoices, targetDate);
- final LocalDate invoiceDate = context.toLocalDate(clock.getUTCNow());
+ final LocalDate invoiceDate = context.toLocalDate(context.getCreatedDate());
final Invoice invoice = new DefaultInvoice(account.getId(), invoiceDate, adjustedTargetDate, targetCurrency);
final UUID invoiceId = invoice.getId();
final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
index 6dc31d6..6a22b6a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/FixedAndRecurringInvoiceItemGenerator.java
@@ -20,6 +20,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -40,6 +41,7 @@ import org.killbill.billing.catalog.api.PhaseType;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
import org.killbill.billing.invoice.model.InvalidDateSequenceException;
@@ -59,6 +61,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
+import com.google.common.collect.Range;
import com.google.inject.Inject;
import static org.killbill.billing.invoice.generator.InvoiceDateUtils.calculateNumberOfWholeBillingPeriods;
@@ -104,10 +107,16 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
final List<InvoiceItem> proposedItems = new ArrayList<InvoiceItem>();
processRecurringBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, perSubscriptionFutureNotificationDate, existingInvoices, internalCallContext);
processFixedBillingEvents(invoiceId, account.getId(), eventSet, targetDate, targetCurrency, proposedItems, internalCallContext);
- accountItemTree.mergeWithProposedItems(proposedItems);
+
+ try {
+ accountItemTree.mergeWithProposedItems(proposedItems);
+ } catch (final IllegalStateException e) {
+ // Proposed items have already been logged
+ throw new InvoiceApiException(e, ErrorCode.UNEXPECTED_ERROR, String.format("ILLEGAL INVOICING STATE accountItemTree=%s", accountItemTree.toString()));
+ }
final List<InvoiceItem> resultingItems = accountItemTree.getResultingItemList();
- safetyBound(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
return resultingItems;
}
@@ -403,8 +412,42 @@ public class FixedAndRecurringInvoiceItemGenerator extends InvoiceItemGenerator
}
}
- // Trigger an exception if we create too many subscriptions for a subscription on a given day
- private void safetyBound(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ @VisibleForTesting
+ void safetyBounds(final Iterable<InvoiceItem> resultingItems, final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription, final InternalTenantContext internalCallContext) throws InvoiceApiException {
+ // Trigger an exception if we detect the creation of similar items for a given subscription
+ // See https://github.com/killbill/killbill/issues/664
+ if (config.isSanitySafetyBoundEnabled(internalCallContext)) {
+ final Map<UUID, Multimap<LocalDate, InvoiceItem>> fixedItemsPerDateAndSubscription = new HashMap<UUID, Multimap<LocalDate, InvoiceItem>>();
+ final Map<UUID, Multimap<Range<LocalDate>, InvoiceItem>> recurringItemsPerServicePeriodAndSubscription = new HashMap<UUID, Multimap<Range<LocalDate>, InvoiceItem>>();
+ for (final InvoiceItem resultingItem : resultingItems) {
+ if (resultingItem.getInvoiceItemType() == InvoiceItemType.FIXED) {
+ if (fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ fixedItemsPerDateAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<LocalDate, InvoiceItem>create());
+ }
+ fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).put(resultingItem.getStartDate(), resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = fixedItemsPerDateAndSubscription.get(resultingItem.getSubscriptionId()).get(resultingItem.getStartDate());
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple FIXED items for subscriptionId='%s', startDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingInvoiceItems));
+ }
+ } else if (resultingItem.getInvoiceItemType() == InvoiceItemType.RECURRING) {
+ if (recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()) == null) {
+ recurringItemsPerServicePeriodAndSubscription.put(resultingItem.getSubscriptionId(), LinkedListMultimap.<Range<LocalDate>, InvoiceItem>create());
+ }
+ final Range<LocalDate> interval = Range.<LocalDate>closedOpen(resultingItem.getStartDate(), resultingItem.getEndDate());
+ recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).put(interval, resultingItem);
+
+ final Collection<InvoiceItem> resultingInvoiceItems = recurringItemsPerServicePeriodAndSubscription.get(resultingItem.getSubscriptionId()).get(interval);
+ if (resultingInvoiceItems.size() > 1) {
+ throw new InvoiceApiException(ErrorCode.UNEXPECTED_ERROR, String.format("SAFETY BOUND TRIGGERED Multiple RECURRING items for subscriptionId='%s', startDate='%s', endDate='%s', resultingItems=%s",
+ resultingItem.getSubscriptionId(), resultingItem.getStartDate(), resultingItem.getEndDate(), resultingInvoiceItems));
+ }
+ }
+ }
+ }
+
+ // Trigger an exception if we create too many invoice items for a subscription on a given day
if (config.getMaxDailyNumberOfItemsSafetyBound(internalCallContext) == -1) {
// Safety bound disabled
return;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
index 3673a4c..3473252 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/generator/InvoiceWithMetadata.java
@@ -45,7 +45,7 @@ public class InvoiceWithMetadata {
this.invoice = originalInvoice;
this.perSubscriptionFutureNotificationDates = perSubscriptionFutureNotificationDates;
build();
- remove$0RecurringAndUsageItems();
+ remove$0UsageItems();
}
public Invoice getInvoice() {
@@ -79,12 +79,12 @@ public class InvoiceWithMetadata {
});
}
- protected void remove$0RecurringAndUsageItems() {
+ protected void remove$0UsageItems() {
if (invoice != null) {
final Iterator<InvoiceItem> it = invoice.getInvoiceItems().iterator();
while (it.hasNext()) {
final InvoiceItem item = it.next();
- if ((item.getInvoiceItemType() == InvoiceItemType.RECURRING || item.getInvoiceItemType() == InvoiceItemType.USAGE) &&
+ if ((item.getInvoiceItemType() == InvoiceItemType.USAGE) &&
item.getAmount().compareTo(BigDecimal.ZERO) == 0) {
it.remove();
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index c29c28a..cd2c7da 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -484,18 +484,9 @@ public class InvoiceDispatcher {
final Iterable<InvoiceItemModelDao> invoiceItemModelDaos,
final FutureAccountNotifications futureAccountNotifications,
final boolean isRealInvoiceWithItems, final InternalCallContext context) throws SubscriptionBaseApiException, InvoiceApiException {
- // We filter any zero amount for USAGE items prior we generate the invoice, which may leave us with an invoice with no items;
- // we recompute the isRealInvoiceWithItems flag based on what is left (the call to invoice is still necessary to set the future notifications).
- final Iterable<InvoiceItemModelDao> filteredInvoiceItemModelDaos = Iterables.filter(invoiceItemModelDaos, new Predicate<InvoiceItemModelDao>() {
- @Override
- public boolean apply(@Nullable final InvoiceItemModelDao input) {
- return (input.getType() != InvoiceItemType.USAGE || input.getAmount().compareTo(BigDecimal.ZERO) != 0);
- }
- });
-
- final boolean isThereAnyItemsLeft = filteredInvoiceItemModelDaos.iterator().hasNext();
+ final boolean isThereAnyItemsLeft = invoiceItemModelDaos.iterator().hasNext();
if (isThereAnyItemsLeft) {
- invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(filteredInvoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
+ invoiceDao.createInvoice(invoiceModelDao, ImmutableList.copyOf(invoiceItemModelDaos), isRealInvoiceWithItems, futureAccountNotifications, context);
} else {
invoiceDao.setFutureAccountNotificationsForEmptyInvoice(account.getId(), futureAccountNotifications, context);
}
@@ -732,7 +723,6 @@ public class InvoiceDispatcher {
BigDecimal childInvoiceAmount = InvoiceCalculatorUtils.computeChildInvoiceAmount(childInvoice.getCurrency(), childInvoice.getInvoiceItems());
InvoiceModelDao draftParentInvoice = invoiceDao.getParentDraftInvoice(account.getParentAccountId(), parentContext);
- final DateTime today = clock.getNow(account.getTimeZone());
final String description = account.getExternalKey().concat(" summary");
if (draftParentInvoice != null) {
@@ -746,7 +736,7 @@ public class InvoiceDispatcher {
}
// new item when the parent invoices does not have this child item yet
- final ParentInvoiceItem newParentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
+ final ParentInvoiceItem newParentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), context.getCreatedDate(), draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
draftParentInvoice.addInvoiceItem(new InvoiceItemModelDao(newParentInvoiceItem));
List<InvoiceModelDao> invoices = new ArrayList<InvoiceModelDao>();
@@ -757,8 +747,9 @@ public class InvoiceDispatcher {
return;
}
- draftParentInvoice = new InvoiceModelDao(account.getParentAccountId(), today.toLocalDate(), account.getCurrency(), InvoiceStatus.DRAFT, true);
- InvoiceItem parentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), today, draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
+ final LocalDate invoiceDate = context.toLocalDate(context.getCreatedDate());
+ draftParentInvoice = new InvoiceModelDao(account.getParentAccountId(), invoiceDate, account.getCurrency(), InvoiceStatus.DRAFT, true);
+ final InvoiceItem parentInvoiceItem = new ParentInvoiceItem(UUID.randomUUID(), context.getCreatedDate(), draftParentInvoice.getId(), account.getParentAccountId(), account.getId(), childInvoiceAmount, account.getCurrency(), description);
draftParentInvoice.addInvoiceItem(new InvoiceItemModelDao(parentInvoiceItem));
// build account date time zone
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
index 6db52ff..5b200f4 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/model/DefaultInvoice.java
@@ -73,7 +73,6 @@ public class DefaultInvoice extends EntityBase implements Invoice, Cloneable {
this(invoiceId, null, accountId, invoiceNumber, invoiceDate, targetDate, currency, currency, isMigrationInvoice, false, status, false, null);
}
-
// This CTOR is used to return an existing invoice and must include everything (items, payments, tags,..)
public DefaultInvoice(final InvoiceModelDao invoiceModelDao) {
this(invoiceModelDao.getId(), invoiceModelDao.getCreatedDate(), invoiceModelDao.getAccountId(),
@@ -95,7 +94,6 @@ public class DefaultInvoice extends EntityBase implements Invoice, Cloneable {
}));
}
- // Used to create a new parent invoice
public DefaultInvoice(final UUID accountId, final LocalDate invoiceDate, final Currency currency) {
this(UUID.randomUUID(), null, accountId, null, invoiceDate, null, currency, currency, false, false, InvoiceStatus.DRAFT, true, null);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java
index def85b1..e7ab1f8 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/AccountItemTree.java
@@ -209,4 +209,12 @@ public class AccountItemTree {
}
}).orNull();
}
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("AccountItemTree{");
+ sb.append("subscriptionItemTree=").append(subscriptionItemTree);
+ sb.append('}');
+ return sb.toString();
+ }
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
index 1b4ff98..8c31b4b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/Item.java
@@ -143,12 +143,12 @@ public class Item {
}
public void incrementAdjustedAmount(final BigDecimal increment) {
- Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0, "Invalid adjustment increment='%s', item=%s", increment, this);
adjustedAmount = adjustedAmount.add(increment);
}
public void incrementCurrentRepairedAmount(final BigDecimal increment) {
- Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0);
+ Preconditions.checkState(increment.compareTo(BigDecimal.ZERO) > 0, "Invalid repair increment='%s', item=%s", increment, this);
currentRepairedAmount = currentRepairedAmount.add(increment);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java
index c217011..9140329 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsInterval.java
@@ -106,7 +106,7 @@ public class ItemsInterval {
for (final UUID invoiceItemId : cancellingPairPerInvoiceItemId.keySet()) {
final Collection<Item> itemsToRemove = cancellingPairPerInvoiceItemId.get(invoiceItemId);
- Preconditions.checkArgument(itemsToRemove.size() <= 2, "Too many repairs for invoiceItemId='%s': %s", invoiceItemId, itemsToRemove);
+ Preconditions.checkState(itemsToRemove.size() <= 2, "Too many repairs for invoiceItemId='%s': %s", invoiceItemId, itemsToRemove);
if (itemsToRemove.size() == 2) {
for (final Item itemToRemove : itemsToRemove) {
items.remove(itemToRemove);
@@ -126,6 +126,15 @@ public class ItemsInterval {
});
}
+ public Iterable<Item> get_CANCEL_items() {
+ return Iterables.filter(items, new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getAction() == ItemAction.CANCEL;
+ }
+ });
+ }
+
public NodeInterval getNodeInterval() {
return interval;
}
@@ -189,14 +198,24 @@ public class ItemsInterval {
items.remove(item);
}
- public Item getCancelledItemIfExists(final UUID targetId) {
- final Item item = Iterables.tryFind(items, new Predicate<Item>() {
- @Override
- public boolean apply(final Item input) {
- return input.getAction() == ItemAction.CANCEL && input.getLinkedId().equals(targetId);
- }
- }).orNull();
- return item;
+ public Item getCancellingItemIfExists(final UUID targetId) {
+ return Iterables.tryFind(items,
+ new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getAction() == ItemAction.CANCEL && input.getLinkedId().equals(targetId);
+ }
+ }).orNull();
+ }
+
+ public Item getCancelledItemIfExists(final UUID linkedId) {
+ return Iterables.tryFind(items,
+ new Predicate<Item>() {
+ @Override
+ public boolean apply(final Item input) {
+ return input.getAction() == ItemAction.ADD && input.getId().equals(linkedId);
+ }
+ }).orNull();
}
public int size() {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
index 4106f87..51b2b26 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/ItemsNodeInterval.java
@@ -24,7 +24,6 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -298,6 +297,7 @@ public class ItemsNodeInterval extends NodeInterval {
// When we detect such nodes, we delete both the ADD in the parent interval and the CANCEL in the children (and cleanup the interval if it does not have items)
//
private void pruneTree() {
+ final NodeInterval root = this;
walkTree(new WalkCallback() {
@Override
public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
@@ -314,6 +314,36 @@ public class ItemsNodeInterval extends NodeInterval {
curNode.getParent().removeChild(curNode);
}
+ for (final Item curCancelItem : curNodeItems.get_CANCEL_items()) {
+ // Sanity: cancelled items should only be in the same node or parents
+ if (curNode.getLeftChild() != null) {
+ curNode.getLeftChild()
+ .walkTree(new WalkCallback() {
+ @Override
+ public void onCurrentNode(final int depth, final NodeInterval curNode, final NodeInterval parent) {
+ final ItemsInterval curChildItems = ((ItemsNodeInterval) curNode).getItemsInterval();
+ final Item cancelledItem = curChildItems.getCancelledItemIfExists(curCancelItem.getLinkedId());
+ if (cancelledItem != null) {
+ throw new IllegalStateException(String.format("Invalid cancelledItem=%s for cancelItem=%s", cancelledItem, curCancelItem));
+ }
+ }
+ });
+ }
+
+ // Sanity: make sure the CANCEL item points to an ADD item
+ final NodeInterval nodeIntervalForCancelledItem = root.findNode(new SearchCallback() {
+ @Override
+ public boolean isMatch(final NodeInterval curNode) {
+ final ItemsInterval curChildItems = ((ItemsNodeInterval) curNode).getItemsInterval();
+ final Item cancelledItem = curChildItems.getCancelledItemIfExists(curCancelItem.getLinkedId());
+ return cancelledItem != null;
+ }
+ });
+ if (nodeIntervalForCancelledItem == null) {
+ throw new IllegalStateException(String.format("Missing cancelledItem for cancelItem=%s", curCancelItem));
+ }
+ }
+
if (!curNode.isPartitionedByChildren()) {
return;
}
@@ -337,7 +367,7 @@ public class ItemsNodeInterval extends NodeInterval {
boolean foundFullRepairByParts = curChild != null;
while (curChild != null) {
final ItemsInterval curChildItems = ((ItemsNodeInterval) curChild).getItemsInterval();
- Item cancellingItem = curChildItems.getCancelledItemIfExists(curAddItem.getId());
+ Item cancellingItem = curChildItems.getCancellingItemIfExists(curAddItem.getId());
if (cancellingItem == null) {
foundFullRepairByParts = false;
break;
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
index de29b6e..70effcf 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/tree/SubscriptionItemTree.java
@@ -274,6 +274,23 @@ public class SubscriptionItemTree {
}
@Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("SubscriptionItemTree{");
+ sb.append("targetInvoiceId=").append(targetInvoiceId);
+ sb.append(", subscriptionId=").append(subscriptionId);
+ sb.append(", root=").append(root);
+ sb.append(", isBuilt=").append(isBuilt);
+ sb.append(", isMerged=").append(isMerged);
+ sb.append(", items=").append(items);
+ sb.append(", existingFullyAdjustedItems=").append(existingFullyAdjustedItems);
+ sb.append(", existingFixedItems=").append(existingFixedItems);
+ sb.append(", remainingFixedItems=").append(remainingFixedItems);
+ sb.append(", pendingItemAdj=").append(pendingItemAdj);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ @Override
public boolean equals(final Object o) {
if (this == o) {
return true;
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
index 1487f3c..606f8bc 100644
--- a/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/ddl.sql
@@ -77,8 +77,12 @@ CREATE TABLE invoice_payments (
CREATE UNIQUE INDEX invoice_payments_id ON invoice_payments(id);
CREATE INDEX invoice_payments_invoice_id ON invoice_payments(invoice_id);
CREATE INDEX invoice_payments_reversals ON invoice_payments(linked_invoice_payment_id);
+CREATE INDEX invoice_payments_payment_id ON invoice_payments(payment_id);
+CREATE INDEX invoice_payments_payment_cookie_id ON invoice_payments(payment_cookie_id);
CREATE INDEX invoice_payments_tenant_account_record_id ON invoice_payments(tenant_record_id, account_record_id);
+
+
DROP TABLE IF EXISTS invoice_parent_children;
CREATE TABLE invoice_parent_children (
record_id serial unique,
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20161208173933__invoice_payment_idx.sql b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20161208173933__invoice_payment_idx.sql
new file mode 100644
index 0000000..a9c3c99
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20161208173933__invoice_payment_idx.sql
@@ -0,0 +1,2 @@
+CREATE INDEX invoice_payments_payment_id ON invoice_payments(payment_id);
+CREATE INDEX invoice_payments_payment_cookie_id ON invoice_payments(payment_cookie_id);
diff --git a/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20164030010821__invoice_default_status_459.sql b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20164030010821__invoice_default_status_459.sql
new file mode 100644
index 0000000..be13ebb
--- /dev/null
+++ b/invoice/src/main/resources/org/killbill/billing/invoice/migration/V20164030010821__invoice_default_status_459.sql
@@ -0,0 +1 @@
+alter table invoices modify column status varchar(15) NOT NULL DEFAULT 'COMMITTED';
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
index b26dbbb..1bf8600 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/dao/TestInvoiceDao.java
@@ -818,7 +818,7 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
createCredit(accountId, clock.getUTCToday(), new BigDecimal("20.0"));
final String description = UUID.randomUUID().toString();
- final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD);
+ final InvoiceModelDao invoiceForExternalCharge = new InvoiceModelDao(accountId, clock.getUTCToday(), clock.getUTCToday(), Currency.USD, false);
final InvoiceItemModelDao externalCharge = new InvoiceItemModelDao(new ExternalChargeInvoiceItem(invoiceForExternalCharge.getId(), accountId, bundleId, description, clock.getUTCToday(), new BigDecimal("15.0"), Currency.USD));
invoiceForExternalCharge.addInvoiceItem(externalCharge);
final InvoiceItemModelDao charge = invoiceDao.createInvoices(ImmutableList.<InvoiceModelDao>of(invoiceForExternalCharge), context).get(0);
@@ -1704,7 +1704,6 @@ public class TestInvoiceDao extends InvoiceTestSuiteWithEmbeddedDB {
final InvoiceModelDao invoiceModelDao;
if (invoiceId == null) {
invoiceModelDao = new InvoiceModelDao(accountId, effectiveDate, effectiveDate, Currency.USD, false, InvoiceStatus.DRAFT);
-
} else {
invoiceModelDao = invoiceDao.getById(invoiceId, context);
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
index 9aeedfe..e6eb41f 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestDefaultInvoiceGenerator.java
@@ -123,6 +123,16 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
}
@Override
+ public boolean isSanitySafetyBoundEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isSanitySafetyBoundEnabled(final InternalTenantContext tenantContext) {
+ return true;
+ }
+
+ @Override
public int getMaxDailyNumberOfItemsSafetyBound() {
return 10;
}
@@ -252,18 +262,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 10, 31));
}
- private SubscriptionBase createSubscription() {
- return createSubscription(UUID.randomUUID(), UUID.randomUUID());
- }
-
- private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
- final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
- Mockito.when(sub.getId()).thenReturn(subscriptionId);
- Mockito.when(sub.getBundleId()).thenReturn(bundleId);
-
- return sub;
- }
-
@Test(groups = "fast")
public void testSimpleWithTimeZone() throws InvoiceApiException, CatalogApiException {
final SubscriptionBase sub = createSubscription();
@@ -629,7 +627,9 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
final Invoice invoice = invoiceWithMetadata.getInvoice();
- assertNull(invoice);
+ assertNotNull(invoice);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
}
@Test(groups = "fast")
@@ -840,65 +840,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
generator.generateInvoice(account, events, null, targetDate, Currency.USD, internalCallContext);
}
- private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.THIRTY_DAYS);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase() {
- return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
- @Nullable final BigDecimal fixedCost,
- final PhaseType phaseType) {
- final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
- final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
-
- return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
- }
-
- private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
- return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
- null, BillingPeriod.ANNUAL, phaseType);
- }
-
- private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
- final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
- final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
- final Currency currency = Currency.USD;
-
- return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
- planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
- planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
- currency, planPhase.getRecurring().getBillingPeriod(),
- billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", 1L, SubscriptionBaseTransitionType.CREATE);
- }
-
- private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
- final LocalDate targetDate, final int expectedNumberOfItems,
- final BigDecimal expectedAmount) throws InvoiceApiException {
- final Currency currency = Currency.USD;
- final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
- final Invoice invoice = invoiceWithMetadata.getInvoice();
- assertNotNull(invoice);
- assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
- existingInvoices.add(invoice);
-
- distributeItems(existingInvoices);
- assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
- }
-
@Test(groups = "fast")
public void testWithFullRepairInvoiceGeneration() throws CatalogApiException, InvoiceApiException {
final LocalDate april25 = new LocalDate(2012, 4, 25);
@@ -1108,32 +1049,6 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(newInvoice);
}
- private void distributeItems(final List<Invoice> invoices) {
- final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
-
- for (final Invoice invoice : invoices) {
- invoiceMap.put(invoice.getId(), invoice);
- }
-
- for (final Invoice invoice : invoices) {
- final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
- final UUID invoiceId = invoice.getId();
-
- while (itemIterator.hasNext()) {
- final InvoiceItem item = itemIterator.next();
-
- if (!item.getInvoiceId().equals(invoiceId)) {
- final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
- if (thisInvoice == null) {
- throw new NullPointerException();
- }
- thisInvoice.addInvoiceItem(item);
- itemIterator.remove();
- }
- }
- }
- }
-
@Test(groups = "fast")
public void testAutoInvoiceOffAccount() throws Exception {
final MockBillingEventSet events = new MockBillingEventSet();
@@ -1156,6 +1071,7 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoiceWithMetadata.getInvoice());
}
+ @Test(groups = "fast")
public void testAutoInvoiceOffWithCredits() throws CatalogApiException, InvoiceApiException {
final Currency currency = Currency.USD;
final List<Invoice> invoices = new ArrayList<Invoice>();
@@ -1255,6 +1171,305 @@ public class TestDefaultInvoiceGenerator extends InvoiceTestSuiteNoDB {
assertNull(invoice2);
}
+ // Complex but plausible scenario, with multiple same-day changes, to verify bounds are not triggered
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testMultipleDailyChangesDoNotTriggerBounds() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.DISCOUNT);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 4, 5);
+
+ final Plan plan2 = new MockPlan("plan2");
+ final PlanPhase plan2Phase1 = createMockMonthlyPlanPhase(null, TWENTY, PhaseType.TRIAL);
+ final PlanPhase plan2Phase2 = createMockMonthlyPlanPhase(THIRTY, PhaseType.DISCOUNT);
+ final PlanPhase plan2Phase3 = createMockMonthlyPlanPhase(FORTY, PhaseType.EVERGREEN);
+ final PlanPhase plan2Phase4 = createMockMonthlyPlanPhase();
+ final LocalDate plan2PhaseChangeToEvergreenDate = invoiceUtil.buildDate(2011, 6, 5);
+ final LocalDate plan2CancelDate = invoiceUtil.buildDate(2011, 6, 5);
+
+ // On 1/5/2011, start TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // On 1/5/2011, change to TRIAL on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan2, plan2Phase1, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, TWENTY);
+ assertEquals(invoices.get(0), invoice);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWENTY), 0);
+
+ // On 1/5/2011, change back to TRIAL on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+
+ // We don't repair FIXED items and one already exists for that date - nothing to generate
+ testNullInvoiceGeneration(events, invoices, plan1StartDate);
+
+ // On 4/5/2011, phase change to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ assertEquals(invoices.get(1), invoice);
+ invoice = invoices.get(2);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+
+ // On 4/5/2011, change to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(2), invoice);
+ invoice = invoices.get(3);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(2).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan1
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("-18"));
+ assertEquals(invoices.get(3), invoice);
+ invoice = invoices.get(4);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(3).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(THIRTY.negate()), 0);
+
+ // On 4/5/2011, change back to DISCOUNT on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan2, plan2Phase2, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 2, new BigDecimal("18"));
+ assertEquals(invoices.get(4), invoice);
+ invoice = invoices.get(5);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getLinkedItemId(), invoices.get(4).getInvoiceItems().get(0).getId());
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 4, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(TWELVE.negate()), 0);
+
+ // On 6/5/2011, phase change to EVERGREEN on plan2
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2PhaseChangeToEvergreenDate, plan2, plan2Phase3, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 2, new BigDecimal("70"));
+ assertEquals(invoices.get(5), invoice);
+ invoice = invoices.get(6);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 5, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(THIRTY), 0);
+ assertEquals(invoice.getInvoiceItems().get(1).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(1).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(1).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(1).getAmount().compareTo(FORTY), 0);
+
+ // On 6/5/2011, cancel subscription
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan2CancelDate, plan2, plan2Phase4, 5));
+
+ testInvoiceGeneration(accountId, events, invoices, plan2PhaseChangeToEvergreenDate, 1, FORTY.negate());
+ assertEquals(invoices.get(6), invoice);
+ invoice = invoices.get(7);
+ assertEquals(invoice.getInvoiceItems().get(0).getLinkedItemId(), invoices.get(6).getInvoiceItems().get(1).getId());
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.REPAIR_ADJ);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 6, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 7, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(FORTY.negate()), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testBuggyBillingEventsDoNotImpactInvoicing() throws InvoiceApiException, CatalogApiException {
+ final UUID accountId = UUID.randomUUID();
+ final UUID bundleId = UUID.randomUUID();
+ final UUID subscriptionId1 = UUID.randomUUID();
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final List<Invoice> invoices = new ArrayList<Invoice>();
+ Invoice invoice;
+
+ final Plan plan1 = new MockPlan("plan1");
+ final PlanPhase plan1Phase1 = createMockMonthlyPlanPhase(null, EIGHT, PhaseType.TRIAL);
+ final PlanPhase plan1Phase2 = createMockMonthlyPlanPhase(TWELVE, PhaseType.EVERGREEN);
+ final LocalDate plan1StartDate = invoiceUtil.buildDate(2011, 1, 5);
+ final LocalDate plan1PhaseChangeDate = invoiceUtil.buildDate(2011, 2, 5);
+
+ // To simulate a bug, duplicate the billing events
+ for (int i = 0; i < 10; i++) {
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1StartDate, plan1, plan1Phase1, 5));
+ events.add(createBillingEvent(subscriptionId1, bundleId, plan1PhaseChangeDate, plan1, plan1Phase2, 5));
+ }
+ assertEquals(events.size(), 20);
+
+ // Fix for https://github.com/killbill/killbill/issues/467 will prevent duplicate fixed items
+ testInvoiceGeneration(accountId, events, invoices, plan1StartDate, 1, EIGHT);
+ invoice = invoices.get(0);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 1, 5));
+ assertNull(invoice.getInvoiceItems().get(0).getEndDate());
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(EIGHT), 0);
+
+ // Intermediate billing intervals associated with recurring items will be less than a day, so only one recurring item will be generated
+ testInvoiceGeneration(accountId, events, invoices, plan1PhaseChangeDate, 1, TWELVE);
+ invoice = invoices.get(1);
+ assertEquals(invoice.getInvoiceItems().size(), 1);
+ assertEquals(invoice.getInvoiceItems().get(0).getSubscriptionId(), subscriptionId1);
+ assertEquals(invoice.getInvoiceItems().get(0).getInvoiceItemType(), InvoiceItemType.RECURRING);
+ assertEquals(invoice.getInvoiceItems().get(0).getStartDate(), new LocalDate(2011, 2, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getEndDate(), new LocalDate(2011, 3, 5));
+ assertEquals(invoice.getInvoiceItems().get(0).getAmount().compareTo(TWELVE), 0);
+ }
+
+ private Long totalOrdering = 1L;
+
+ private MockPlanPhase createMockThirtyDaysPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.THIRTY_DAYS);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase() {
+ return new MockPlanPhase(null, null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockMonthlyPlanPhase(@Nullable final BigDecimal recurringRate,
+ @Nullable final BigDecimal fixedCost,
+ final PhaseType phaseType) {
+ final MockInternationalPrice recurringPrice = (recurringRate == null) ? null : new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD));
+ final MockInternationalPrice fixedPrice = (fixedCost == null) ? null : new MockInternationalPrice(new DefaultPrice(fixedCost, Currency.USD));
+
+ return new MockPlanPhase(recurringPrice, fixedPrice, BillingPeriod.MONTHLY, phaseType);
+ }
+
+ private MockPlanPhase createMockAnnualPlanPhase(final BigDecimal recurringRate, final PhaseType phaseType) {
+ return new MockPlanPhase(new MockInternationalPrice(new DefaultPrice(recurringRate, Currency.USD)),
+ null, BillingPeriod.ANNUAL, phaseType);
+ }
+
+ private SubscriptionBase createSubscription() {
+ return createSubscription(UUID.randomUUID(), UUID.randomUUID());
+ }
+
+ private SubscriptionBase createSubscription(final UUID subscriptionId, final UUID bundleId) {
+ final SubscriptionBase sub = Mockito.mock(SubscriptionBase.class);
+ Mockito.when(sub.getId()).thenReturn(subscriptionId);
+ Mockito.when(sub.getBundleId()).thenReturn(bundleId);
+
+ return sub;
+ }
+
+ private BillingEvent createBillingEvent(final UUID subscriptionId, final UUID bundleId, final LocalDate startDate,
+ final Plan plan, final PlanPhase planPhase, final int billCycleDayLocal) throws CatalogApiException {
+ final SubscriptionBase sub = createSubscription(subscriptionId, bundleId);
+ final Currency currency = Currency.USD;
+
+ return invoiceUtil.createMockBillingEvent(null, sub, startDate.toDateTimeAtStartOfDay(), plan, planPhase,
+ planPhase.getFixed().getPrice() == null ? null : planPhase.getFixed().getPrice().getPrice(currency),
+ planPhase.getRecurring().getRecurringPrice() == null ? null : planPhase.getRecurring().getRecurringPrice().getPrice(currency),
+ currency, planPhase.getRecurring().getBillingPeriod(),
+ billCycleDayLocal, BillingMode.IN_ADVANCE, "Test", totalOrdering++, SubscriptionBaseTransitionType.CREATE);
+ }
+
+ private void testInvoiceGeneration(final UUID accountId, final BillingEventSet events, final List<Invoice> existingInvoices,
+ final LocalDate targetDate, final int expectedNumberOfItems,
+ final BigDecimal expectedAmount) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNotNull(invoice);
+ assertEquals(invoice.getNumberOfItems(), expectedNumberOfItems);
+ existingInvoices.add(invoice);
+
+ distributeItems(existingInvoices);
+ assertEquals(invoice.getBalance(), KillBillMoney.of(expectedAmount, invoice.getCurrency()));
+ }
+
+ private void testNullInvoiceGeneration(final BillingEventSet events, final List<Invoice> existingInvoices, final LocalDate targetDate) throws InvoiceApiException {
+ final Currency currency = Currency.USD;
+ final InvoiceWithMetadata invoiceWithMetadata = generator.generateInvoice(account, events, existingInvoices, targetDate, currency, internalCallContext);
+ final Invoice invoice = invoiceWithMetadata.getInvoice();
+ assertNull(invoice);
+ }
+
+ private void distributeItems(final List<Invoice> invoices) {
+ final Map<UUID, Invoice> invoiceMap = new HashMap<UUID, Invoice>();
+
+ for (final Invoice invoice : invoices) {
+ invoiceMap.put(invoice.getId(), invoice);
+ }
+
+ for (final Invoice invoice : invoices) {
+ final Iterator<InvoiceItem> itemIterator = invoice.getInvoiceItems().iterator();
+ final UUID invoiceId = invoice.getId();
+
+ while (itemIterator.hasNext()) {
+ final InvoiceItem item = itemIterator.next();
+
+ if (!item.getInvoiceId().equals(invoiceId)) {
+ final Invoice thisInvoice = invoiceMap.get(item.getInvoiceId());
+ if (thisInvoice == null) {
+ throw new NullPointerException();
+ }
+ thisInvoice.addInvoiceItem(item);
+ itemIterator.remove();
+ }
+ }
+ }
+ }
+
private void printDetailInvoice(final Invoice invoice) {
log.info("-------------------- START DETAIL ----------------------");
log.info("Invoice " + invoice.getId() + ": BALANCE = " + invoice.getBalance()
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
index 5fbfc79..26d30ff 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestFixedAndRecurringInvoiceItemGenerator.java
@@ -19,6 +19,7 @@ package org.killbill.billing.invoice.generator;
import java.math.BigDecimal;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -47,7 +48,9 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.invoice.model.FixedPriceInvoiceItem;
+import org.killbill.billing.invoice.model.ItemAdjInvoiceItem;
import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.invoice.model.RepairAdjInvoiceItem;
import org.killbill.billing.junction.BillingEvent;
import org.killbill.billing.junction.BillingEventSet;
import org.killbill.billing.subscription.api.SubscriptionBase;
@@ -55,6 +58,9 @@ import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
@@ -285,7 +291,7 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
}
@Test(groups = "fast")
- public void testSafetyBounds() throws InvoiceApiException {
+ public void testSafetyBoundsTooManyInvoiceItemsForGivenSubscriptionAndInvoiceDate() throws InvoiceApiException {
final int threshold = 15;
final LocalDate startDate = new LocalDate("2016-01-01");
@@ -372,4 +378,639 @@ public class TestFixedAndRecurringInvoiceItemGenerator extends InvoiceTestSuiteN
assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
}
}
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDate() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(null, price, BillingPeriod.NO_BILLING_PERIOD, PhaseType.TRIAL);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ amount,
+ null,
+ account.getCurrency(),
+ BillingPeriod.NO_BILLING_PERIOD,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of fixed items for that subscription and start date (simulate bad data on disk)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ "Buggy fixed item",
+ startDate,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ // There will be one proposed, but because it will match one of ones in the existing list and we don't repair, it won't be returned
+ assertEquals(generatedItems.size(), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testSubscriptionAlreadyDoubleBilledForServicePeriod() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a bunch of recurring items for that subscription and service period (bad data on disk leading to double billing)
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ for (int i = 0; i < 20; i++) {
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate.plusMonths(i), account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ // Set random dates to verify it doesn't impact double billing detection
+ startDate.plusMonths(i).toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusMonths(1),
+ amount,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+ }
+
+ try {
+ // There will be one proposed item but the tree will refuse the merge because of the bad state on disk
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Double billing detected"));
+ }
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testOverlappingItems() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a previous mis-bill: existing item is for [2016-01-01,2016-01-30], proposed will be for [2016-01-01,2016-02-01]
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusDays(29),
+ amount,
+ amount,
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+
+ // We will repair the wrong item and generate the correct recurring item
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ assertEquals(generatedItems.size(), 2);
+ assertTrue(generatedItems.get(0) instanceof RecurringInvoiceItem);
+ assertEquals(generatedItems.get(0).getStartDate(), new LocalDate("2016-01-01"));
+ assertEquals(generatedItems.get(0).getEndDate(), new LocalDate("2016-02-01"));
+ assertEquals(generatedItems.get(0).getAmount().compareTo(amount), 0);
+ assertTrue(generatedItems.get(1) instanceof RepairAdjInvoiceItem);
+ assertEquals(generatedItems.get(1).getAmount().compareTo(amount.negate()), 0);
+ assertEquals(generatedItems.get(1).getLinkedItemId(), invoice.getInvoiceItems().get(0).getId());
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testOverlappingItemsWithRepair() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a previous mis-bill: existing item is for [2016-01-01,2016-01-30], proposed will be for [2016-01-01,2016-02-01]
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusDays(29),
+ amount,
+ amount,
+ account.getCurrency()));
+ // But the system has already repaired it
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusDays(29),
+ BigDecimal.ONE.negate(), // Note! The amount will not matter
+ account.getCurrency(),
+ invoice.getInvoiceItems().get(0).getId()));
+ existingInvoices.add(invoice);
+
+ // We will generate the correct recurring item
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ assertEquals(generatedItems.size(), 1);
+ assertTrue(generatedItems.get(0) instanceof RecurringInvoiceItem);
+ assertEquals(generatedItems.get(0).getStartDate(), new LocalDate("2016-01-01"));
+ assertEquals(generatedItems.get(0).getEndDate(), new LocalDate("2016-02-01"));
+ assertEquals(generatedItems.get(0).getAmount().compareTo(amount), 0);
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testOverlappingItemsWithTooManyRepairs() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a previous mis-bill: existing item is for [2016-01-01,2016-01-30], proposed will be for [2016-01-01,2016-02-01]
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusDays(29),
+ amount,
+ amount,
+ account.getCurrency()));
+ // But the system has already repaired it
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusDays(29),
+ BigDecimal.ONE.negate(), // Note! The amount will not matter
+ account.getCurrency(),
+ invoice.getInvoiceItems().get(0).getId()));
+ // Twice!
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusDays(29),
+ BigDecimal.ONE.negate(), // Note! The amount will not matter
+ account.getCurrency(),
+ invoice.getInvoiceItems().get(0).getId()));
+ existingInvoices.add(invoice);
+
+ try {
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Too many repairs"));
+ }
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testOverlappingItemsWithInvalidRepair() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(amount, account.getCurrency()));
+ final Plan plan = new MockPlan("my-plan");
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ startDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ amount,
+ account.getCurrency(),
+ BillingPeriod.MONTHLY,
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+ events.add(event);
+
+ // Simulate a previous mis-bill: existing item is for [2016-01-01,2016-01-30], proposed will be for [2016-01-01,2016-02-01]
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ startDate,
+ startDate.plusDays(29),
+ amount,
+ amount,
+ account.getCurrency()));
+ // Also, the system has repaired a bigger period
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusDays(30),
+ BigDecimal.ONE.negate(), // Amount does not matter
+ account.getCurrency(),
+ invoice.getInvoiceItems().get(0).getId()));
+ existingInvoices.add(invoice);
+
+ try {
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Invalid cancelledItem"));
+ }
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testInvalidRepair() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ // Dangling repair
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusMonths(1),
+ BigDecimal.ONE.negate(),
+ account.getCurrency(),
+ UUID.randomUUID()));
+ existingInvoices.add(invoice);
+
+ try {
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Missing cancelledItem"));
+ }
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testInvalidAdjustment() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ // Dangling adjustment
+ invoice.addInvoiceItem(new ItemAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ "Dangling adjustment",
+ BigDecimal.ONE.negate(),
+ account.getCurrency(),
+ UUID.randomUUID()));
+ existingInvoices.add(invoice);
+
+ try {
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Missing subscription id"));
+ }
+ }
+
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testItemFullyRepairedAndFullyAdjusted() throws InvoiceApiException {
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final BillingEventSet events = new MockBillingEventSet();
+ final BigDecimal amount = BigDecimal.TEN;
+
+ // Subscription incorrectly invoiced
+ final List<Invoice> existingInvoices = new LinkedList<Invoice>();
+ final Invoice invoice = new DefaultInvoice(account.getId(), clock.getUTCToday(), startDate, account.getCurrency());
+ invoice.addInvoiceItem(new RecurringInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "my-plan",
+ "my-plan-monthly",
+ startDate,
+ startDate.plusMonths(1),
+ amount,
+ amount,
+ account.getCurrency()));
+ // Repaired by the system
+ invoice.addInvoiceItem(new RepairAdjInvoiceItem(UUID.randomUUID(),
+ startDate.toDateTimeAtStartOfDay(),
+ invoice.getId(),
+ account.getId(),
+ startDate,
+ startDate.plusMonths(1),
+ BigDecimal.ONE.negate(),
+ account.getCurrency(),
+ invoice.getInvoiceItems().get(0).getId()));
+ invoice.addInvoiceItem(new ItemAdjInvoiceItem(invoice.getInvoiceItems().get(0),
+ startDate,
+ amount.negate(), // Note! The amount will matter
+ account.getCurrency()));
+ existingInvoices.add(invoice);
+
+ try {
+ final List<InvoiceItem> generatedItems = fixedAndRecurringInvoiceItemGenerator.generateItems(account,
+ UUID.randomUUID(),
+ events,
+ existingInvoices,
+ startDate,
+ account.getCurrency(),
+ new HashMap<UUID, SubscriptionFutureNotificationDates>(),
+ internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ assertTrue(e.getCause().getMessage().startsWith("Too many repairs"));
+ }
+ }
+
+ // Simulate a bug in the generator where two fixed items for the same day and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyFixedInvoiceItemsForGivenSubscriptionAndStartDatePostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem fixedPriceInvoiceItem = new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(fixedPriceInvoiceItem);
+ resultingItems.add(fixedPriceInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new FixedPriceInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ "description",
+ startDate,
+ // Amount shouldn't have any effect
+ BigDecimal.ONE.add(new BigDecimal(i)),
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
+
+ // Simulate a bug in the generator where two recurring items for the same service period and subscription end up in the resulting items
+ @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/664")
+ public void testTooManyRecurringInvoiceItemsForGivenSubscriptionAndServicePeriodPostMerge() throws InvoiceApiException {
+ final Multimap<UUID, LocalDate> createdItemsPerDayPerSubscription = LinkedListMultimap.<UUID, LocalDate>create();
+ final LocalDate startDate = new LocalDate("2016-01-01");
+
+ final Collection<InvoiceItem> resultingItems = new LinkedList<InvoiceItem>();
+ final InvoiceItem recurringInvoiceItem = new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ BigDecimal.ONE,
+ BigDecimal.ONE,
+ account.getCurrency());
+ resultingItems.add(recurringInvoiceItem);
+ resultingItems.add(recurringInvoiceItem);
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+
+ resultingItems.clear();
+ for (int i = 0; i < 2; i++) {
+ resultingItems.add(new RecurringInvoiceItem(UUID.randomUUID(),
+ clock.getUTCNow(),
+ null,
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ "planName",
+ "phaseName",
+ startDate,
+ startDate.plusMonths(1),
+ // Amount shouldn't have any effect
+ BigDecimal.TEN,
+ BigDecimal.ONE,
+ account.getCurrency()));
+ }
+
+ try {
+ fixedAndRecurringInvoiceItemGenerator.safetyBounds(resultingItems, createdItemsPerDayPerSubscription, internalCallContext);
+ fail();
+ } catch (final InvoiceApiException e) {
+ assertEquals(e.getCode(), ErrorCode.UNEXPECTED_ERROR.getCode());
+ }
+ }
}
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java
new file mode 100644
index 0000000..0bf40e0
--- /dev/null
+++ b/invoice/src/test/java/org/killbill/billing/invoice/generator/TestInvoiceWithMetadata.java
@@ -0,0 +1,137 @@
+/*
+ * 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.invoice.generator;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import org.joda.time.LocalDate;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.DefaultPrice;
+import org.killbill.billing.catalog.MockInternationalPrice;
+import org.killbill.billing.catalog.MockPlan;
+import org.killbill.billing.catalog.MockPlanPhase;
+import org.killbill.billing.catalog.api.BillingMode;
+import org.killbill.billing.catalog.api.BillingPeriod;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
+import org.killbill.billing.invoice.InvoiceTestSuiteNoDB;
+import org.killbill.billing.invoice.api.Invoice;
+import org.killbill.billing.invoice.api.InvoiceItem;
+import org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates;
+import org.killbill.billing.invoice.model.DefaultInvoice;
+import org.killbill.billing.invoice.model.RecurringInvoiceItem;
+import org.killbill.billing.junction.BillingEvent;
+import org.killbill.billing.subscription.api.SubscriptionBase;
+import org.killbill.billing.subscription.api.SubscriptionBaseTransitionType;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+
+public class TestInvoiceWithMetadata extends InvoiceTestSuiteNoDB {
+
+
+ private Account account;
+ private SubscriptionBase subscription;
+
+ @Override
+ @BeforeMethod(groups = "fast")
+ public void beforeMethod() {
+ super.beforeMethod();
+
+ try {
+ account = invoiceUtil.createAccount(callContext);
+ subscription = invoiceUtil.createSubscription();
+ } catch (final Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testWith$0RecurringItem() {
+
+
+ final LocalDate invoiceDate = new LocalDate(2016, 11, 15);
+
+ final Invoice originalInvoice = new DefaultInvoice(account.getId(), invoiceDate, account.getCurrency());
+
+ final Plan plan = new MockPlan("my-plan");
+ final MockInternationalPrice price = new MockInternationalPrice(new DefaultPrice(BigDecimal.TEN, account.getCurrency()));
+
+ final PlanPhase planPhase = new MockPlanPhase(price, null, BillingPeriod.MONTHLY, PhaseType.EVERGREEN);
+
+ final BillingEvent event = invoiceUtil.createMockBillingEvent(account,
+ subscription,
+ invoiceDate.toDateTimeAtStartOfDay(),
+ plan,
+ planPhase,
+ null,
+ BigDecimal.ZERO,
+ account.getCurrency(),
+ planPhase.getRecurring().getBillingPeriod(),
+ 1,
+ BillingMode.IN_ADVANCE,
+ "Billing Event Desc",
+ 1L,
+ SubscriptionBaseTransitionType.CREATE);
+
+
+ final InvoiceItem invoiceItem = new RecurringInvoiceItem(UUID.randomUUID(),
+ invoiceDate.toDateTimeAtStartOfDay(),
+ originalInvoice.getId(),
+ account.getId(),
+ subscription.getBundleId(),
+ subscription.getId(),
+ event.getPlan().getName(),
+ event.getPlanPhase().getName(),
+ invoiceDate,
+ invoiceDate.plusMonths(1),
+ BigDecimal.ZERO,
+ BigDecimal.ZERO,
+ account.getCurrency());
+
+ originalInvoice.addInvoiceItem(invoiceItem);
+
+ final Map<UUID, SubscriptionFutureNotificationDates> perSubscriptionFutureNotificationDates = new HashMap<UUID, SubscriptionFutureNotificationDates>();
+ final SubscriptionFutureNotificationDates subscriptionFutureNotificationDates = new SubscriptionFutureNotificationDates(BillingMode.IN_ADVANCE);
+ subscriptionFutureNotificationDates.updateNextRecurringDateIfRequired(invoiceDate.plusMonths(1));
+
+ perSubscriptionFutureNotificationDates.put(subscription.getId(), subscriptionFutureNotificationDates);
+
+ final InvoiceWithMetadata invoiceWithMetadata = new InvoiceWithMetadata(originalInvoice, perSubscriptionFutureNotificationDates);
+
+ // We generate an invoice with one item, invoicing for $0
+ final Invoice resultingInvoice = invoiceWithMetadata.getInvoice();
+ Assert.assertNotNull(resultingInvoice);
+ Assert.assertEquals(resultingInvoice.getInvoiceItems().size(), 1);
+ Assert.assertEquals(resultingInvoice.getInvoiceItems().get(0).getAmount().compareTo(BigDecimal.ZERO), 0);
+
+ final Map<UUID, InvoiceWithMetadata.SubscriptionFutureNotificationDates> dateMap = invoiceWithMetadata.getPerSubscriptionFutureNotificationDates();
+
+ final InvoiceWithMetadata.SubscriptionFutureNotificationDates futureNotificationDates = dateMap.get(subscription.getId());
+
+ // We verify that we generated the future notification for a month ahead
+ Assert.assertNotNull(futureNotificationDates.getNextRecurringDate());
+ Assert.assertEquals(futureNotificationDates.getNextRecurringDate().compareTo(invoiceDate.plusMonths(1)), 0 );
+ }
+
+}
\ No newline at end of file
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index 34b3004..f072706 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -366,7 +366,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
public void testProcessedCurrencyExists() throws Exception {
// Use InvoiceModelDao to build the invoice to be able to set the processedCurrency (No suitable CTOR for DefaultInvoice on purpose)
- final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL);
+ final InvoiceModelDao invoiceModelDao = new InvoiceModelDao(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL, false);
invoiceModelDao.setProcessedCurrency(Currency.USD);
final Invoice invoice = new DefaultInvoice(invoiceModelDao);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
index b8467f4..4c4688c 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/tree/TestSubscriptionItemTree.java
@@ -45,6 +45,7 @@ import com.google.common.collect.Lists;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
@@ -62,8 +63,6 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
private final String phaseName = "my-phase";
private final Currency currency = Currency.USD;
-
-
@Test(groups = "fast")
public void testWithExistingSplitRecurring() {
@@ -118,7 +117,6 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
Assert.assertTrue(tree.getView().isEmpty());
}
-
@Test(groups = "fast")
public void testSimpleRepair() {
@@ -166,6 +164,54 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testInvalidRepair() {
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal rate = new BigDecimal("12.00");
+
+ final InvoiceItem initial = new RecurringInvoiceItem(invoiceId, accountId, bundleId, subscriptionId, planName, phaseName, startDate, endDate, rate, rate, currency);
+ final InvoiceItem tooEarlyRepair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate.minusDays(1), endDate, rate.negate(), currency, initial.getId());
+ final InvoiceItem tooLateRepair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate, endDate.plusDays(1), rate.negate(), currency, initial.getId());
+
+ SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(initial);
+ tree.addItem(tooEarlyRepair);
+ try {
+ tree.build();
+ fail();
+ } catch (final IllegalStateException e) {
+ }
+
+ tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(initial);
+ tree.addItem(tooLateRepair);
+ try {
+ tree.build();
+ fail();
+ } catch (final IllegalStateException e) {
+ }
+ }
+
+ @Test(groups = "fast")
+ public void testDanglingRepair() {
+ final LocalDate startDate = new LocalDate(2014, 1, 1);
+ final LocalDate endDate = new LocalDate(2014, 2, 1);
+
+ final BigDecimal rate = new BigDecimal("12.00");
+
+ final InvoiceItem repair = new RepairAdjInvoiceItem(invoiceId, accountId, startDate.minusDays(1), endDate, rate.negate(), currency, UUID.randomUUID());
+
+ final SubscriptionItemTree tree = new SubscriptionItemTree(subscriptionId, invoiceId);
+ tree.addItem(repair);
+ try {
+ tree.build();
+ fail();
+ } catch (final IllegalStateException e) {
+ }
+ }
+
+ @Test(groups = "fast")
public void testMultipleRepair() {
final LocalDate startDate = new LocalDate(2014, 1, 1);
@@ -498,7 +544,6 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
verifyResult(tree.getView(), expectedResult);
}
-
// Will test the case A from ItemsNodeInterval#prune logic (an item is left on the interval)
@Test(groups = "fast")
public void testFullRepairPruneLogic2() {
@@ -544,7 +589,6 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
}
-
// Will test the case B from ItemsNodeInterval#prune logic
@Test(groups = "fast")
public void testFullRepairByPartsPruneLogic1() {
@@ -643,8 +687,6 @@ public class TestSubscriptionItemTree extends InvoiceTestSuiteNoDB {
verifyResult(tree.getView(), expectedResult);
}
-
-
@Test(groups = "fast")
public void testMergeWithNoExisting() {
jaxrs/pom.xml 2(+1 -1)
diff --git a/jaxrs/pom.xml b/jaxrs/pom.xml
index 45a9f4b..c878e78 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-jaxrs</artifactId>
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/CatalogJson.java
index 1f44b5d..381089a 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
@@ -86,6 +86,10 @@ public class CatalogJson {
for (final Plan plan : plans) {
// Build the product associated with this plan
final Product product = plan.getProduct();
+ if (product == null) {
+ // TODO Should we import a logger here?
+ continue;
+ }
ProductJson productJson = productMap.get(product.getName());
if (productJson == null) {
productJson = new ProductJson(product.getCategory().toString(),
@@ -133,10 +137,8 @@ public class CatalogJson {
private List<UsageJson> buildUsagesJson(final Usage[] usages) throws CurrencyValueNull {
List<UsageJson> usagesJson = new ArrayList<UsageJson>();
- if (usages != null && usages.length > 0) {
- for (int i=0; i < usages.length; i++) {
- usagesJson.add(new UsageJson(usages[i].getBillingPeriod().toString(), buildTiers(usages[i].getTiers())));
- }
+ for (int i = 0; i < usages.length; i++) {
+ usagesJson.add(new UsageJson(usages[i].getBillingPeriod().toString(), buildTiers(usages[i].getTiers())));
}
return usagesJson;
}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java
index 0b8249c..371d9f3 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PlanDetailJson.java
@@ -67,7 +67,7 @@ public class PlanDetailJson {
if (plan.getFinalPhase() == null ||
plan.getFinalPhase().getRecurring() == null ||
plan.getFinalPhase().getRecurring().getRecurringPrice() == null ||
- plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices() == null) {
+ plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length == 0) {
this.finalPhaseRecurringPrice = ImmutableList.<PriceJson>of();
} else {
this.finalPhaseRecurringPrice = Lists.transform(ImmutableList.<Price>copyOf(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()),
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
index 13e4b82..46a69f0 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/SubscriptionJson.java
@@ -30,7 +30,6 @@ import org.killbill.billing.ObjectType;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Currency;
-import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
@@ -377,21 +376,22 @@ public class SubscriptionJson extends JsonBase {
this.subscriptionId = subscription.getId().toString();
this.externalKey = subscription.getExternalKey();
this.events = new LinkedList<EventSubscriptionJson>();
- for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
- this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
- }
-
// We fill the catalog info every time we get the currency from the account (even if this is not overridden Plan)
this.priceOverrides = new ArrayList<PhasePriceOverrideJson>();
- if (currency != null) {
- final Plan plan = subscription.getLastActivePlan();
- if (plan != null) {
- for (final PlanPhase cur : plan.getAllPhases()) {
- final BigDecimal fixedPrice = cur.getFixed() != null ? cur.getFixed().getPrice().getPrice(currency) : null;
- final BigDecimal recurringPrice = cur.getRecurring() != null ? cur.getRecurring().getRecurringPrice().getPrice(currency) : null;
- final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(cur.getName(), cur.getPhaseType().toString(), fixedPrice, recurringPrice, cur.getUsages(),currency);
- priceOverrides.add(phase);
+ String currentPhaseName = null;
+ for (final SubscriptionEvent subscriptionEvent : subscription.getSubscriptionEvents()) {
+ this.events.add(new EventSubscriptionJson(subscriptionEvent, accountAuditLogs));
+ if (currency != null) {
+ final PlanPhase cur = subscriptionEvent.getNextPhase();
+ if (cur == null || cur.getName().equals(currentPhaseName)) {
+ continue;
}
+ currentPhaseName = cur.getName();
+
+ final BigDecimal fixedPrice = cur.getFixed() != null ? cur.getFixed().getPrice().getPrice(currency) : null;
+ final BigDecimal recurringPrice = cur.getRecurring() != null ? cur.getRecurring().getRecurringPrice().getPrice(currency) : null;
+ final PhasePriceOverrideJson phase = new PhasePriceOverrideJson(cur.getName(), cur.getPhaseType().toString(), fixedPrice, recurringPrice, cur.getUsages(),currency);
+ priceOverrides.add(phase);
}
}
}
diff --git a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJson.java b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJson.java
index f3e0934..243d97f 100644
--- a/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJson.java
+++ b/jaxrs/src/test/java/org/killbill/billing/jaxrs/json/TestPlanDetailJson.java
@@ -18,6 +18,7 @@ package org.killbill.billing.jaxrs.json;
import java.util.UUID;
+import org.killbill.billing.catalog.DefaultPrice;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.InternationalPrice;
import org.killbill.billing.catalog.api.Listing;
@@ -27,6 +28,7 @@ import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.Recurring;
import org.killbill.billing.jaxrs.JaxrsTestSuiteNoDB;
+import org.mockito.Mock;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -63,6 +65,7 @@ public class TestPlanDetailJson extends JaxrsTestSuiteNoDB {
Mockito.when(product.getName()).thenReturn(UUID.randomUUID().toString());
final InternationalPrice price = Mockito.mock(InternationalPrice.class);
+ Mockito.when(price.getPrices()).thenReturn(new DefaultPrice[0]);
final PlanPhase planPhase = Mockito.mock(PlanPhase.class);
final Recurring recurring = Mockito.mock(Recurring.class);
Mockito.when(recurring.getRecurringPrice()).thenReturn(price);
junction/pom.xml 2(+1 -1)
diff --git a/junction/pom.xml b/junction/pom.xml
index 6206cee..996d9e8 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-junction</artifactId>
diff --git a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
index 7183e3c..9b500aa 100644
--- a/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
+++ b/junction/src/main/java/org/killbill/billing/junction/plumbing/billing/DefaultBillingEvent.java
@@ -340,7 +340,7 @@ public class DefaultBillingEvent implements BillingEvent {
if (!isActive) {
return result;
}
- if (planPhase != null && planPhase.getUsages() != null) {
+ if (planPhase != null && planPhase.getUsages().length > 0) {
result = Lists.newArrayList();
for (Usage usage : planPhase.getUsages()) {
result.add(usage);
diff --git a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
index ebb438e..e8f80d2 100644
--- a/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
+++ b/junction/src/test/java/org/killbill/billing/junction/plumbing/billing/TestBlockingCalculator.java
@@ -32,6 +32,7 @@ import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.DefaultUsage;
import org.killbill.billing.catalog.MockPlan;
import org.killbill.billing.catalog.api.BillingPeriod;
import org.killbill.billing.catalog.api.CatalogApiException;
@@ -552,6 +553,7 @@ public class TestBlockingCalculator extends JunctionTestSuiteNoDB {
final Recurring recurring = Mockito.mock(Recurring.class);
Mockito.when(recurring.getRecurringPrice()).thenReturn(recurringPrice);
Mockito.when(planPhase.getRecurring()).thenReturn(recurring);
+ Mockito.when(planPhase.getUsages()).thenReturn(new DefaultUsage[0]);
final BigDecimal fixedPrice = BigDecimal.TEN;
NEWS 6(+6 -0)
diff --git a/NEWS b/NEWS
index 857acaa..9edb09e 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,9 @@
+0.18.1
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.1
+
+0.18.0
+ See https://github.com/killbill/killbill/releases/tag/killbill-0.18.0
+
0.17.8
See https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
overdue/pom.xml 2(+1 -1)
diff --git a/overdue/pom.xml b/overdue/pom.xml
index 2abb00d..43076b0 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-overdue</artifactId>
payment/pom.xml 2(+1 -1)
diff --git a/payment/pom.xml b/payment/pom.xml
index 07af64e..31b7a5d 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-payment</artifactId>
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
index 9058564..9ebad50 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultAdminPaymentApi.java
@@ -17,24 +17,40 @@
package org.killbill.billing.payment.api;
+import java.util.List;
+
import javax.annotation.Nullable;
import javax.inject.Inject;
import org.killbill.billing.callcontext.InternalCallContext;
import org.killbill.billing.payment.core.PaymentTransactionInfoPluginConverter;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentAttemptTask;
+import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
+import org.killbill.billing.payment.dao.PaymentAttemptModelDao;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.PaymentConfig;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPaymentApi {
+ private final PaymentStateMachineHelper paymentSMHelper;
+ private final IncompletePaymentAttemptTask incompletePaymentAttemptTask;
private final PaymentDao paymentDao;
private final InternalCallContextFactory internalCallContextFactory;
@Inject
- public DefaultAdminPaymentApi(final PaymentConfig paymentConfig, final PaymentDao paymentDao, final InternalCallContextFactory internalCallContextFactory) {
+ public DefaultAdminPaymentApi(final PaymentConfig paymentConfig,
+ final PaymentStateMachineHelper paymentSMHelper,
+ final IncompletePaymentAttemptTask incompletePaymentAttemptTask,
+ final PaymentDao paymentDao,
+ final InternalCallContextFactory internalCallContextFactory) {
super(paymentConfig, internalCallContextFactory);
+ this.paymentSMHelper = paymentSMHelper;
+ this.incompletePaymentAttemptTask = incompletePaymentAttemptTask;
this.paymentDao = paymentDao;
this.internalCallContextFactory = internalCallContextFactory;
}
@@ -42,19 +58,54 @@ public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPayme
@Override
public void fixPaymentTransactionState(final Payment payment,
final PaymentTransaction paymentTransaction,
- @Nullable final TransactionStatus transactionStatusMaybeNull,
- @Nullable final String lastSuccessPaymentState,
- final String currentPaymentStateName,
+ @Nullable final TransactionStatus transactionStatusOrNull,
+ @Nullable final String lastSuccessPaymentStateOrNull,
+ @Nullable final String currentPaymentStateNameOrNull,
final Iterable<PluginProperty> properties,
final CallContext callContext) throws PaymentApiException {
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(payment.getAccountId(), callContext);
- final TransactionStatus transactionStatus;
- if (transactionStatusMaybeNull == null) {
+ TransactionStatus transactionStatus = transactionStatusOrNull;
+ if (transactionStatusOrNull == null) {
checkNotNullParameter(paymentTransaction.getPaymentInfoPlugin(), "PaymentTransactionInfoPlugin");
transactionStatus = PaymentTransactionInfoPluginConverter.toTransactionStatus(paymentTransaction.getPaymentInfoPlugin());
- } else {
- transactionStatus = transactionStatusMaybeNull;
+ }
+
+ String currentPaymentStateName = currentPaymentStateNameOrNull;
+ if (currentPaymentStateName == null) {
+ switch (transactionStatus) {
+ case PENDING:
+ currentPaymentStateName = paymentSMHelper.getPendingStateForTransaction(paymentTransaction.getTransactionType());
+ break;
+ case SUCCESS:
+ currentPaymentStateName = paymentSMHelper.getSuccessfulStateForTransaction(paymentTransaction.getTransactionType());
+ break;
+ case PAYMENT_FAILURE:
+ currentPaymentStateName = paymentSMHelper.getFailureStateForTransaction(paymentTransaction.getTransactionType());
+ break;
+ case PLUGIN_FAILURE:
+ case UNKNOWN:
+ default:
+ currentPaymentStateName = paymentSMHelper.getErroredStateForTransaction(paymentTransaction.getTransactionType());
+ break;
+ }
+ }
+
+ String lastSuccessPaymentState = lastSuccessPaymentStateOrNull;
+ if (lastSuccessPaymentState == null &&
+ // Verify we are not updating an older transaction (only the last one has an impact on lastSuccessPaymentState)
+ paymentTransaction.getId().equals(payment.getTransactions().get(payment.getTransactions().size() - 1).getId())) {
+ if (paymentSMHelper.isSuccessState(currentPaymentStateName)) {
+ lastSuccessPaymentState = currentPaymentStateName;
+ } else {
+ for (int i = payment.getTransactions().size() - 2; i >= 0; i--) {
+ final PaymentTransaction transaction = payment.getTransactions().get(i);
+ if (TransactionStatus.SUCCESS.equals(transaction.getTransactionStatus())) {
+ lastSuccessPaymentState = paymentSMHelper.getSuccessfulStateForTransaction(transaction.getTransactionType());
+ break;
+ }
+ }
+ }
}
paymentDao.updatePaymentAndTransactionOnCompletion(payment.getAccountId(),
@@ -70,5 +121,20 @@ public class DefaultAdminPaymentApi extends DefaultApiBase implements AdminPayme
paymentTransaction.getGatewayErrorCode(),
paymentTransaction.getGatewayErrorMsg(),
internalCallContext);
+
+ // If there is a payment attempt associated with that transaction, we need to update it as well
+ final List<PaymentAttemptModelDao> paymentAttemptsModelDao = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransaction.getExternalKey(), internalCallContext);
+ final PaymentAttemptModelDao paymentAttemptModelDao = Iterables.<PaymentAttemptModelDao>tryFind(paymentAttemptsModelDao,
+ new Predicate<PaymentAttemptModelDao>() {
+ @Override
+ public boolean apply(final PaymentAttemptModelDao input) {
+ return paymentTransaction.getId().equals(input.getTransactionId());
+ }
+ }).orNull();
+ if (paymentAttemptModelDao != null) {
+ // We can re-use the logic from IncompletePaymentAttemptTask as it is doing very similar work (i.e. run the completion part of
+ // the state machine to call the plugins and update the attempt in the right terminal state)
+ incompletePaymentAttemptTask.doIteration(paymentAttemptModelDao);
+ }
}
}
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 b4bb5f4..c98a029 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
@@ -722,7 +722,50 @@ public class DefaultPaymentApi extends DefaultApiBase implements PaymentApi {
@Override
public Payment notifyPendingTransactionOfStateChangedWithPaymentControl(final Account account, final UUID paymentTransactionId, final boolean isSuccess, final PaymentOptions paymentOptions, final CallContext callContext) throws PaymentApiException {
- throw new IllegalStateException("Not implemented");
+ final List<String> paymentControlPluginNames = toPaymentControlPluginNames(paymentOptions, callContext);
+ if (paymentControlPluginNames.isEmpty()) {
+ return notifyPendingTransactionOfStateChanged(account, paymentTransactionId, isSuccess, callContext);
+ }
+
+ checkNotNullParameter(account, "account");
+ checkNotNullParameter(paymentTransactionId, "paymentTransactionId");
+
+ final String transactionType = "NOTIFY_STATE_CHANGE";
+ Payment payment = null;
+ PaymentTransaction paymentTransaction = null;
+ PaymentApiException exception = null;
+ try {
+ logEnterAPICall(log, transactionType, account, null, null, paymentTransactionId, null, null, null, null, null, paymentControlPluginNames);
+
+ final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(account.getId(), callContext);
+ payment = pluginControlPaymentProcessor.notifyPendingPaymentOfStateChanged(IS_API_PAYMENT, account, paymentTransactionId, isSuccess, paymentControlPluginNames, callContext, internalCallContext);
+
+ paymentTransaction = Iterables.<PaymentTransaction>tryFind(payment.getTransactions(),
+ new Predicate<PaymentTransaction>() {
+ @Override
+ public boolean apply(final PaymentTransaction transaction) {
+ return transaction.getId().equals(paymentTransactionId);
+ }
+ }).orNull();
+ return payment;
+ } catch (final PaymentApiException e) {
+ exception = e;
+ throw e;
+ } finally {
+ logExitAPICall(log,
+ transactionType,
+ account,
+ payment != null ? payment.getPaymentMethodId() : null,
+ payment != null ? payment.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getId() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedAmount() : null,
+ paymentTransaction != null ? paymentTransaction.getProcessedCurrency() : null,
+ payment != null ? payment.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getExternalKey() : null,
+ paymentTransaction != null ? paymentTransaction.getTransactionStatus() : null,
+ paymentControlPluginNames,
+ exception);
+ }
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
index 20a0655..26bce39 100644
--- a/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/api/DefaultPaymentGatewayApi.java
@@ -142,9 +142,22 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
try {
priorCallResult = controlPluginRunner.executePluginPriorCalls(account,
paymentMethodId,
- null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, true, paymentControlPluginNames, properties, callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ properties,
+ callContext);
} catch (final PaymentControlApiAbortException e) {
throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_API_ABORTED, e.getPluginName());
@@ -156,16 +169,42 @@ public class DefaultPaymentGatewayApi extends DefaultApiBase implements PaymentG
final T result = callback.doPaymentGatewayApiOperation(priorCallResult.getAdjustedPaymentMethodId(), priorCallResult.getAdjustedPluginProperties());
controlPluginRunner.executePluginOnSuccessCalls(account,
paymentMethodId,
- null, null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
return PluginDispatcher.createPluginDispatcherReturnType(result);
} catch (final PaymentApiException e) {
controlPluginRunner.executePluginOnFailureCalls(account,
paymentMethodId,
- null, null, null, null,
- PaymentApiType.HPP, null, HPPType.BUILD_FORM_DESCRIPTOR,
- null, null, true, paymentControlPluginNames, priorCallResult.getAdjustedPluginProperties(), callContext);
+ null,
+ null,
+ null,
+ null,
+ null,
+ PaymentApiType.HPP,
+ null,
+ HPPType.BUILD_FORM_DESCRIPTOR,
+ null,
+ null,
+ null,
+ null,
+ true,
+ paymentControlPluginNames,
+ priorCallResult.getAdjustedPluginProperties(),
+ callContext);
throw e;
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
index a6b0780..71fee5f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/CompletionTaskBase.java
@@ -100,6 +100,10 @@ abstract class CompletionTaskBase<T> implements Runnable {
}
}
+ public synchronized void start() {
+ this.isStopped = false;
+ }
+
public synchronized void stop() {
this.isStopped = true;
}
@@ -119,15 +123,22 @@ abstract class CompletionTaskBase<T> implements Runnable {
}
protected <T> T doJanitorOperationWithAccountLock(final JanitorIterationCallback callback, final InternalTenantContext internalTenantContext) {
+ try {
+ return tryToDoJanitorOperationWithAccountLock(callback, internalTenantContext);
+ } catch (final LockFailedException e) {
+ log.warn("Error locking accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
+ }
+ return null;
+ }
+
+ protected <T> T tryToDoJanitorOperationWithAccountLock(final JanitorIterationCallback callback, final InternalTenantContext internalTenantContext) throws LockFailedException {
GlobalLock lock = null;
try {
final ImmutableAccountData account = accountInternalApi.getImmutableAccountDataByRecordId(internalTenantContext.getAccountRecordId(), internalTenantContext);
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), paymentConfig.getMaxGlobalLockRetries());
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), paymentConfig.getMaxGlobalLockRetries());
return callback.doIteration();
- } catch (AccountApiException e) {
+ } catch (final AccountApiException e) {
log.warn("Error retrieving accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
- } catch (LockFailedException e) {
- log.warn("Error locking accountRecordId='{}'", internalTenantContext.getAccountRecordId(), e);
} finally {
if (lock != null) {
lock.release();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
index a33e4e0..568ec94 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentAttemptTask.java
@@ -117,17 +117,7 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
log.warn("Found {} transactions for paymentAttempt {}", filteredTransactions.size(), attempt.getId());
}
final PaymentTransactionModelDao transaction = filteredTransactions.isEmpty() ? null : filteredTransactions.get(0);
-
-
- // In those 3 cases (null transaction, PLUGIN_FAILURE and PAYMENT_FAILURE), we are taking a *shortcut* but this is incorrect; ideally we should call back the priorCall
- // control plugins to decide what to do:
- // * For null transaction and PLUGIN_FAILURE something went wrong before we could even make the payment, so possibly we should inform the control plugin
- // and retry
- // * For PAYMENT_FAILURE, the payment went through but was denied by the gateway, and so this is a different case where a control plugin may want to retry
- //
- if (transaction == null ||
- transaction.getTransactionStatus() == TransactionStatus.PLUGIN_FAILURE ||
- transaction.getTransactionStatus() == TransactionStatus.PAYMENT_FAILURE) {
+ if (transaction == null) {
log.info("Moving attemptId='{}' to ABORTED", attempt.getId());
paymentDao.updatePaymentAttempt(attempt.getId(), attempt.getTransactionId(), "ABORTED", internalCallContext);
return;
@@ -139,43 +129,40 @@ public class IncompletePaymentAttemptTask extends CompletionTaskBase<PaymentAtte
return;
}
- // On SUCCESS, PENDING state we complete the payment control state machine, allowing to call the control plugin onSuccessCall API.
- if (transaction.getTransactionStatus() == TransactionStatus.SUCCESS ||
- transaction.getTransactionStatus() == TransactionStatus.PENDING) {
-
- try {
- log.info("Moving attemptId='{}' to SUCCESS", attempt.getId());
-
- final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
- final boolean isApiPayment = true; // unclear
- final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
- isApiPayment,
- transaction.getPaymentId(),
- attempt.getPaymentExternalKey(),
- transaction.getTransactionExternalKey(),
- transaction.getTransactionType(),
- account,
- attempt.getPaymentMethodId(),
- transaction.getAmount(),
- transaction.getCurrency(),
- PluginPropertySerializer.deserialize(attempt.getPluginProperties()),
- internalCallContext,
- callContext);
-
- paymentStateContext.setAttemptId(attempt.getId()); // Normally set by leavingState Callback
- paymentStateContext.setPaymentTransactionModelDao(transaction); // Normally set by raw state machine
- //
- // Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls call
- // to the PaymentControlPluginApi plugin and transition the state.
- //
- pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
- } catch (final AccountApiException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- } catch (final PluginPropertySerializerException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- } catch (final PaymentApiException e) {
- log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
- }
+ try {
+ log.info("Completing attemptId='{}'", attempt.getId());
+
+ final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
+ final boolean isApiPayment = true; // unclear
+ final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(),
+ isApiPayment,
+ null,
+ transaction.getPaymentId(),
+ attempt.getPaymentExternalKey(),
+ transaction.getId(),
+ transaction.getTransactionExternalKey(),
+ transaction.getTransactionType(),
+ account,
+ attempt.getPaymentMethodId(),
+ transaction.getAmount(),
+ transaction.getCurrency(),
+ PluginPropertySerializer.deserialize(attempt.getPluginProperties()),
+ internalCallContext,
+ callContext);
+
+ paymentStateContext.setAttemptId(attempt.getId()); // Normally set by leavingState Callback
+ paymentStateContext.setPaymentTransactionModelDao(transaction); // Normally set by raw state machine
+ //
+ // Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls / executePluginOnFailureCalls calls
+ // to the PaymentControlPluginApi plugin and transition the state.
+ //
+ pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
+ } catch (final AccountApiException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
+ } catch (final PluginPropertySerializerException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
+ } catch (final PaymentApiException e) {
+ log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
index 5fd6dd2..72f050f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/IncompletePaymentTransactionTask.java
@@ -51,6 +51,7 @@ import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
+import org.killbill.commons.locker.LockFailedException;
import org.killbill.notificationq.api.NotificationEvent;
import org.killbill.notificationq.api.NotificationQueue;
import org.skife.config.TimeSpan;
@@ -93,12 +94,19 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
}
public void processNotification(final JanitorNotificationKey notificationKey, final UUID userToken, final Long accountRecordId, final long tenantRecordId) {
+ try {
+ tryToProcessNotification(notificationKey, userToken, accountRecordId, tenantRecordId);
+ } catch (final LockFailedException e) {
+ log.warn("Error locking accountRecordId='{}', will attempt to retry later", accountRecordId, e);
+ insertNewNotificationForUnresolvedTransactionIfNeeded(notificationKey.getUuidKey(), notificationKey.getAttemptNumber(), userToken, accountRecordId, tenantRecordId);
+ }
+ }
+ public void tryToProcessNotification(final JanitorNotificationKey notificationKey, final UUID userToken, final Long accountRecordId, final long tenantRecordId) throws LockFailedException {
final InternalTenantContext internalTenantContext = internalCallContextFactory.createInternalTenantContext(tenantRecordId, accountRecordId);
- doJanitorOperationWithAccountLock(new JanitorIterationCallback() {
+ tryToDoJanitorOperationWithAccountLock(new JanitorIterationCallback() {
@Override
public Void doIteration() {
-
// State may have changed since we originally retrieved with no lock
final PaymentTransactionModelDao rehydratedPaymentTransaction = paymentDao.getPaymentTransaction(notificationKey.getUuidKey(), internalTenantContext);
@@ -139,7 +147,6 @@ public class IncompletePaymentTransactionTask extends CompletionTaskBase<Payment
return null;
}
}, internalTenantContext);
-
}
@Override
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
index 30456c4..cb340c6 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/janitor/Janitor.java
@@ -24,19 +24,10 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.joda.time.DateTime;
-import org.killbill.billing.account.api.AccountInternalApi;
import org.killbill.billing.events.PaymentInternalEvent;
-import org.killbill.billing.osgi.api.OSGIServiceRegistration;
import org.killbill.billing.payment.core.PaymentExecutors;
-import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
-import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
-import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner;
-import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.glue.DefaultPaymentService;
-import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
-import org.killbill.billing.util.callcontext.InternalCallContextFactory;
import org.killbill.billing.util.config.definition.PaymentConfig;
-import org.killbill.clock.Clock;
import org.killbill.commons.locker.GlobalLocker;
import org.killbill.notificationq.api.NotificationEvent;
import org.killbill.notificationq.api.NotificationQueue;
@@ -60,55 +51,28 @@ public class Janitor {
private final NotificationQueueService notificationQueueService;
private final PaymentConfig paymentConfig;
private final PaymentExecutors paymentExecutors;
- private final Clock clock;
- private final PaymentDao paymentDao;
- private final InternalCallContextFactory internalCallContextFactory;
- private final PaymentStateMachineHelper paymentStateMachineHelper;
- private final PaymentControlStateMachineHelper retrySMHelper;
- private final AccountInternalApi accountInternalApi;
- private final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry;
- private final GlobalLocker locker;
- private final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner;
-
-
-
- private IncompletePaymentAttemptTask incompletePaymentAttemptTask;
- private IncompletePaymentTransactionTask incompletePaymentTransactionTask;
+ private final IncompletePaymentAttemptTask incompletePaymentAttemptTask;
+ private final IncompletePaymentTransactionTask incompletePaymentTransactionTask;
private NotificationQueue janitorQueue;
private ScheduledExecutorService janitorExecutor;
private volatile boolean isStopped;
@Inject
- public Janitor(final InternalCallContextFactory internalCallContextFactory,
- final PaymentDao paymentDao,
- final Clock clock,
- final PaymentStateMachineHelper paymentStateMachineHelper,
- final PaymentControlStateMachineHelper retrySMHelper,
- final AccountInternalApi accountInternalApi,
- final PluginControlPaymentAutomatonRunner pluginControlledPaymentAutomatonRunner,
- final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+ public Janitor(final IncompletePaymentAttemptTask incompletePaymentAttemptTask,
+ final IncompletePaymentTransactionTask incompletePaymentTransactionTask,
final GlobalLocker locker,
final PaymentConfig paymentConfig,
final NotificationQueueService notificationQueueService,
final PaymentExecutors paymentExecutors) {
+ this.incompletePaymentAttemptTask = incompletePaymentAttemptTask;
+ this.incompletePaymentTransactionTask = incompletePaymentTransactionTask;
this.notificationQueueService = notificationQueueService;
this.paymentExecutors = paymentExecutors;
this.paymentConfig = paymentConfig;
- this.internalCallContextFactory = internalCallContextFactory;
- this.paymentDao = paymentDao;
- this.clock = clock;
- this.pluginControlledPaymentAutomatonRunner = pluginControlledPaymentAutomatonRunner;
- this.paymentStateMachineHelper = paymentStateMachineHelper;
- this.retrySMHelper = retrySMHelper;
- this.accountInternalApi = accountInternalApi;
- this.pluginRegistry = pluginRegistry;
- this.locker = locker;
-
}
-
public void initialize() throws NotificationQueueAlreadyExists {
janitorQueue = notificationQueueService.createNotificationQueue(DefaultPaymentService.SERVICE_NAME,
QUEUE_NAME,
@@ -128,36 +92,16 @@ public class Janitor {
}
);
- this.incompletePaymentAttemptTask = new IncompletePaymentAttemptTask(internalCallContextFactory,
- paymentConfig,
- paymentDao,
- clock,
- paymentStateMachineHelper,
- retrySMHelper,
- accountInternalApi,
- pluginControlledPaymentAutomatonRunner,
- pluginRegistry,
- locker);
-
- this.incompletePaymentTransactionTask = new IncompletePaymentTransactionTask(internalCallContextFactory,
- paymentConfig,
- paymentDao,
- clock,
- paymentStateMachineHelper,
- retrySMHelper,
- accountInternalApi,
- pluginRegistry,
- locker);
-
-
incompletePaymentTransactionTask.attachJanitorQueue(janitorQueue);
incompletePaymentAttemptTask.attachJanitorQueue(janitorQueue);
}
public void start() {
-
this.isStopped = false;
+ incompletePaymentAttemptTask.start();
+ incompletePaymentTransactionTask.start();
+
janitorExecutor = paymentExecutors.getJanitorExecutorService();
janitorQueue.startQueue();
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
index 34e881b..ebc4fe5 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentMethodProcessor.java
@@ -110,7 +110,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
return dispatchWithExceptionHandling(account,
paymentPluginServiceName,
new CallableWithAccountLock<UUID, PaymentApiException>(locker,
- account.getExternalKey(),
+ account.getId(),
paymentConfig,
new DispatcherCallback<PluginDispatcherReturnType<UUID>, PaymentApiException>() {
@@ -403,7 +403,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context)
throws PaymentApiException {
try {
- new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
+ new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<Void> doOperation() throws PaymentApiException {
@@ -444,7 +444,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
public void setDefaultPaymentMethod(final Account account, final UUID paymentMethodId, final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context)
throws PaymentApiException {
try {
- new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
+ new WithAccountLock<Void, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<Void>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<Void> doOperation() throws PaymentApiException {
@@ -511,7 +511,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
}
try {
- final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getExternalKey(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {
+ final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<List<PaymentMethod>> doOperation() throws PaymentApiException {
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 6719b7c..3acf35f 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
@@ -369,16 +369,20 @@ public class PaymentProcessor extends ProcessorBase {
return;
}
- final PaymentAttemptModelDao lastPaymentAttempt = attempts.get(attempts.size() - 1);
+ final PaymentAttemptModelDao lastPaymentAttempt = attempts.get(attempts.size() - 1);
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(lastPaymentAttempt.getAccountId(), callContext);
+ cancelScheduledPaymentTransaction(lastPaymentAttempt.getId(), internalCallContext);
+ }
+
+ public void cancelScheduledPaymentTransaction(final UUID lastPaymentAttemptId, final InternalCallContext internalCallContext) throws PaymentApiException {
try {
final NotificationQueue retryQueue = notificationQueueService.getNotificationQueue(DefaultPaymentService.SERVICE_NAME, DefaultRetryService.QUEUE_NAME);
final List<NotificationEventWithMetadata<NotificationEvent>> notificationEventWithMetadatas =
retryQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
for (final NotificationEventWithMetadata<NotificationEvent> notificationEvent : notificationEventWithMetadatas) {
- if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttempt.getId())) {
+ if (((PaymentRetryNotificationKey) notificationEvent.getEvent()).getAttemptId().equals(lastPaymentAttemptId)) {
retryQueue.removeNotification(notificationEvent.getRecordId());
break;
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
index 22e12ff..736fa84 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PluginControlPaymentProcessor.java
@@ -46,6 +46,7 @@ import org.killbill.billing.payment.core.sm.PluginControlPaymentAutomatonRunner.
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;
import org.killbill.billing.payment.dao.PluginPropertySerializer;
import org.killbill.billing.payment.dao.PluginPropertySerializer.PluginPropertySerializerException;
import org.killbill.billing.payment.invoice.InvoicePaymentControlPluginApi;
@@ -198,6 +199,42 @@ public class PluginControlPaymentProcessor extends ProcessorBase {
callContext, internalCallContext);
}
+ public Payment notifyPendingPaymentOfStateChanged(final boolean isApiPayment, final Account account, final UUID paymentTransactionId, final boolean isSuccess, final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentTransactionId, internalCallContext);
+ final List<PaymentAttemptModelDao> attempts = paymentDao.getPaymentAttemptByTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey(), internalCallContext);
+ final PaymentAttemptModelDao attempt = Iterables.find(attempts,
+ new Predicate<PaymentAttemptModelDao>() {
+ @Override
+ public boolean apply(final PaymentAttemptModelDao input) {
+ return input.getTransactionId().equals(paymentTransactionId);
+ }
+ });
+
+ final Iterable<PluginProperty> pluginProperties;
+ try {
+ pluginProperties = PluginPropertySerializer.deserialize(attempt.getPluginProperties());
+ } catch (final PluginPropertySerializerException e) {
+ throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, String.format("Unable to deserialize payment attemptId='%s' properties", attempt.getId()));
+ }
+
+ return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
+ isSuccess,
+ paymentTransactionModelDao.getTransactionType(),
+ ControlOperation.NOTIFICATION_OF_STATE_CHANGE,
+ account,
+ attempt.getPaymentMethodId(),
+ paymentTransactionModelDao.getPaymentId(),
+ attempt.getPaymentExternalKey(),
+ paymentTransactionId,
+ paymentTransactionModelDao.getTransactionExternalKey(),
+ paymentTransactionModelDao.getAmount(),
+ paymentTransactionModelDao.getCurrency(),
+ pluginProperties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
public Payment createChargeback(final boolean isApiPayment, final Account account, final UUID paymentId, final String transactionExternalKey, final BigDecimal amount, final Currency currency,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
return pluginControlledPaymentAutomatonRunner.run(isApiPayment,
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
index a18340b..5866bcc 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/ProcessorBase.java
@@ -163,23 +163,23 @@ public abstract class ProcessorBase {
public static class CallableWithAccountLock<ReturnType, ExceptionType extends Exception> implements Callable<PluginDispatcherReturnType<ReturnType>> {
private final GlobalLocker locker;
- private final String accountExternalKey;
+ private final UUID accountId;
private final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback;
private final PaymentConfig paymentConfig;
public CallableWithAccountLock(final GlobalLocker locker,
- final String accountExternalKey,
+ final UUID accountId,
final PaymentConfig paymentConfig,
final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback) {
this.locker = locker;
- this.accountExternalKey = accountExternalKey;
+ this.accountId = accountId;
this.callback = callback;
this.paymentConfig = paymentConfig;
}
@Override
public PluginDispatcherReturnType<ReturnType> call() throws ExceptionType, LockFailedException {
- return new WithAccountLock<ReturnType, ExceptionType>(paymentConfig).processAccountWithLock(locker, accountExternalKey, callback);
+ return new WithAccountLock<ReturnType, ExceptionType>(paymentConfig).processAccountWithLock(locker, accountId, callback);
}
}
@@ -191,11 +191,11 @@ public abstract class ProcessorBase {
this.paymentConfig = paymentConfig;
}
- public PluginDispatcherReturnType<ReturnType> processAccountWithLock(final GlobalLocker locker, final String accountExternalKey, final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback)
+ public PluginDispatcherReturnType<ReturnType> processAccountWithLock(final GlobalLocker locker, final UUID accountId, final DispatcherCallback<PluginDispatcherReturnType<ReturnType>, ExceptionType> callback)
throws ExceptionType, LockFailedException {
GlobalLock lock = null;
try {
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountExternalKey, paymentConfig.getMaxGlobalLockRetries());
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), paymentConfig.getMaxGlobalLockRetries());
return callback.doOperation();
} finally {
if (lock != null) {
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
index fd8eed7..9ba0ab2 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/CompletionControlOperation.java
@@ -22,19 +22,22 @@ import java.util.List;
import org.killbill.automaton.OperationException;
import org.killbill.automaton.OperationResult;
import org.killbill.billing.control.plugin.api.PaymentApiType;
+import org.killbill.billing.control.plugin.api.PaymentControlContext;
import org.killbill.billing.payment.api.Payment;
import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.api.TransactionStatus;
import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.ProcessorBase.DispatcherCallback;
import org.killbill.billing.payment.core.sm.control.ControlPluginRunner.DefaultPaymentControlContext;
import org.killbill.billing.payment.dao.PaymentTransactionModelDao;
import org.killbill.billing.payment.dispatcher.PluginDispatcher;
import org.killbill.billing.payment.dispatcher.PluginDispatcher.PluginDispatcherReturnType;
-import org.killbill.billing.control.plugin.api.PaymentControlContext;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.commons.locker.GlobalLocker;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
//
// Used from AttemptCompletionTask to resume an incomplete payment that went through control API.
@@ -54,11 +57,11 @@ public class CompletionControlOperation extends OperationControlCallback {
@Override
public OperationResult doOperationCallback() throws OperationException {
-
final List<String> controlPluginNameList = paymentStateControlContext.getPaymentControlPluginNames();
final String controlPluginNames = JOINER.join(controlPluginNameList);
return dispatchWithAccountLockAndTimeout(controlPluginNames, new DispatcherCallback<PluginDispatcherReturnType<OperationResult>, OperationException>() {
+
@Override
public PluginDispatcherReturnType<OperationResult> doOperation() throws OperationException {
final PaymentTransactionModelDao transaction = paymentStateContext.getPaymentTransactionModelDao();
@@ -78,15 +81,34 @@ public class CompletionControlOperation extends OperationControlCallback {
transaction.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentStateContext.getCallContext());
+ try {
+ final Payment result = doCallSpecificOperationCallback();
+ ((PaymentStateControlContext) paymentStateContext).setResult(result);
+
+ final boolean success = transaction.getTransactionStatus() == TransactionStatus.SUCCESS || transaction.getTransactionStatus() == TransactionStatus.PENDING;
+ if (success) {
+ executePluginOnSuccessCalls(paymentStateControlContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
+
+ // Remove scheduled retry, if any
+ paymentProcessor.cancelScheduledPaymentTransaction(paymentStateControlContext.getAttemptId(), paymentStateControlContext.getInternalCallContext());
- executePluginOnSuccessCalls(paymentStateControlContext.getPaymentControlPluginNames(), updatedPaymentControlContext);
- return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
+ return PluginDispatcher.createPluginDispatcherReturnType(OperationResult.SUCCESS);
+ } else {
+ throw new OperationException(null, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ }
+ } catch (final PaymentApiException e) {
+ // Wrap PaymentApiException, and throw a new OperationException with an ABORTED/FAILURE state based on the retry result.
+ throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ } catch (final RuntimeException e) {
+ // Attempts to set the retry date in context if needed.
+ throw new OperationException(e, executePluginOnFailureCallsAndSetRetryDate(updatedPaymentControlContext));
+ }
}
});
}
@Override
protected Payment doCallSpecificOperationCallback() throws PaymentApiException {
- return null;
+ return paymentProcessor.getPayment(paymentStateContext.getPaymentId(), false, false, ImmutableList.<PluginProperty>of(), paymentStateContext.getCallContext(), paymentStateContext.getInternalCallContext());
}
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
index 1ee36fc..d212925 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/ControlPluginRunner.java
@@ -62,12 +62,15 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
+ final UUID paymentTransactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
final HPPType hppType,
final BigDecimal amount,
final Currency currency,
+ final BigDecimal processedAmount,
+ final Currency processedCurrency,
final boolean isApiPayment,
final List<String> paymentControlPluginNames,
final Iterable<PluginProperty> pluginProperties,
@@ -85,12 +88,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
amount,
currency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
@@ -124,12 +130,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
inputAmount,
inputCurrency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
}
@@ -143,7 +152,7 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
- final UUID transactionId,
+ final UUID paymentTransactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
@@ -162,7 +171,7 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
- transactionId,
+ paymentTransactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
@@ -201,12 +210,15 @@ public class ControlPluginRunner {
final UUID paymentAttemptId,
final UUID paymentId,
final String paymentExternalKey,
+ final UUID transactionId,
final String paymentTransactionExternalKey,
final PaymentApiType paymentApiType,
final TransactionType transactionType,
final HPPType hppType,
final BigDecimal amount,
final Currency currency,
+ final BigDecimal processedAmount,
+ final Currency processedCurrency,
final boolean isApiPayment,
final List<String> paymentControlPluginNames,
final Iterable<PluginProperty> pluginProperties,
@@ -217,12 +229,15 @@ public class ControlPluginRunner {
paymentAttemptId,
paymentId,
paymentExternalKey,
+ transactionId,
paymentTransactionExternalKey,
paymentApiType,
transactionType,
hppType,
amount,
currency,
+ processedAmount,
+ processedCurrency,
isApiPayment,
callContext);
@@ -273,15 +288,22 @@ public class ControlPluginRunner {
private final Currency processedCurrency;
private final boolean isApiPayment;
- public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, final String transactionExternalKey,
- final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType, final BigDecimal amount, final Currency currency,
- final boolean isApiPayment, final CallContext callContext) {
- this(account, paymentMethodId, attemptId, paymentId, paymentExternalKey, null, transactionExternalKey, paymentApiType, transactionType, hppType, amount, currency, null, null, isApiPayment, callContext);
- }
-
- public DefaultPaymentControlContext(final Account account, final UUID paymentMethodId, final UUID attemptId, @Nullable final UUID paymentId, final String paymentExternalKey, @Nullable final UUID transactionId, final String transactionExternalKey,
- final PaymentApiType paymentApiType, final TransactionType transactionType, final HPPType hppType,
- final BigDecimal amount, final Currency currency, @Nullable final BigDecimal processedAmount, @Nullable final Currency processedCurrency, final boolean isApiPayment, final CallContext callContext) {
+ public DefaultPaymentControlContext(final Account account,
+ final UUID paymentMethodId,
+ final UUID attemptId,
+ @Nullable final UUID paymentId,
+ final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String transactionExternalKey,
+ final PaymentApiType paymentApiType,
+ final TransactionType transactionType,
+ final HPPType hppType,
+ final BigDecimal amount,
+ final Currency currency,
+ @Nullable final BigDecimal processedAmount,
+ @Nullable final Currency processedCurrency,
+ final boolean isApiPayment,
+ final CallContext callContext) {
super(callContext.getTenantId(), callContext.getUserName(), callContext.getCallOrigin(), callContext.getUserType(), callContext.getReasonCode(), callContext.getComments(), callContext.getUserToken(), callContext.getCreatedDate(), callContext.getUpdatedDate());
this.account = account;
this.paymentMethodId = paymentMethodId;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
index dcd1707..4b35883 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/DefaultControlInitiated.java
@@ -89,7 +89,9 @@ public class DefaultControlInitiated implements LeavingStateCallback {
}
if (paymentTransactionModelDao != null) {
- stateContext.setPaymentTransactionExternalKey(paymentTransactionModelDao.getTransactionExternalKey());
+ stateContext.setPaymentTransactionModelDao(paymentTransactionModelDao);
+ stateContext.setProcessedAmount(paymentTransactionModelDao.getProcessedAmount());
+ stateContext.setProcessedCurrency(paymentTransactionModelDao.getProcessedCurrency());
} else if (stateContext.getPaymentTransactionExternalKey() == null) {
stateContext.setPaymentTransactionExternalKey(UUIDs.randomUUID().toString());
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
new file mode 100644
index 0000000..de2bfbf
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/NotificationOfStateChangeControlOperation.java
@@ -0,0 +1,47 @@
+/*
+ * 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.payment.core.sm.control;
+
+import org.killbill.automaton.OperationResult;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.dispatcher.PluginDispatcher;
+import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.commons.locker.GlobalLocker;
+
+public class NotificationOfStateChangeControlOperation extends OperationControlCallback {
+
+ public NotificationOfStateChangeControlOperation(final GlobalLocker locker,
+ final PluginDispatcher<OperationResult> paymentPluginDispatcher,
+ final PaymentConfig paymentConfig,
+ final PaymentStateControlContext paymentStateContext,
+ final PaymentProcessor paymentProcessor,
+ final ControlPluginRunner controlPluginRunner) {
+ super(locker, paymentPluginDispatcher, paymentStateContext, paymentProcessor, paymentConfig, controlPluginRunner);
+ }
+
+ @Override
+ protected Payment doCallSpecificOperationCallback() throws PaymentApiException {
+ return paymentProcessor.notifyPendingPaymentOfStateChanged(paymentStateControlContext.getAccount(),
+ paymentStateControlContext.getTransactionId(),
+ paymentStateControlContext.isSuccess(),
+ paymentStateControlContext.getCallContext(),
+ paymentStateControlContext.getInternalCallContext());
+ }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
index 4f05018..ce634c0 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/OperationControlCallback.java
@@ -91,12 +91,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
+ paymentStateContext.getTransactionId(),
paymentStateContext.getPaymentTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentStateContext.getTransactionType(),
null,
paymentStateContext.getAmount(),
paymentStateContext.getCurrency(),
+ paymentStateControlContext.getProcessedAmount(),
+ paymentStateControlContext.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentStateContext.getCallContext());
@@ -173,12 +176,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentStateContext.getPaymentId(),
paymentStateContext.getPaymentExternalKey(),
+ paymentStateContext.getTransactionId(),
paymentStateContext.getPaymentTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentStateContext.getTransactionType(),
null,
paymentControlContextArg.getAmount(),
paymentControlContextArg.getCurrency(),
+ paymentControlContextArg.getProcessedAmount(),
+ paymentControlContextArg.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentControlPluginNames,
paymentStateContext.getProperties(),
@@ -212,7 +218,7 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
adjustStateContextPluginProperties(paymentStateContext, result.getAdjustedPluginProperties());
}
- private OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentControlContext paymentControlContext) {
+ protected OperationResult executePluginOnFailureCallsAndSetRetryDate(final PaymentControlContext paymentControlContext) {
final DateTime retryDate = executePluginOnFailureCalls(paymentStateControlContext.getPaymentControlPluginNames(), paymentControlContext);
if (retryDate != null) {
((PaymentStateControlContext) paymentStateContext).setRetryDate(retryDate);
@@ -227,12 +233,15 @@ public abstract class OperationControlCallback extends OperationCallbackBase<Pay
paymentStateControlContext.getAttemptId(),
paymentControlContext.getPaymentId(),
paymentControlContext.getPaymentExternalKey(),
+ paymentControlContext.getTransactionId(),
paymentControlContext.getTransactionExternalKey(),
PaymentApiType.PAYMENT_TRANSACTION,
paymentControlContext.getTransactionType(),
null,
paymentControlContext.getAmount(),
paymentControlContext.getCurrency(),
+ paymentControlContext.getProcessedAmount(),
+ paymentControlContext.getProcessedCurrency(),
paymentStateControlContext.isApiPayment(),
paymentControlPluginNames,
paymentStateContext.getProperties(),
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
index 2841907..920c660 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/control/PaymentStateControlContext.java
@@ -40,16 +40,30 @@ import com.google.common.collect.Iterables;
public class PaymentStateControlContext extends PaymentStateContext {
+ private final Boolean isSuccess;
+
private DateTime retryDate;
private List<String> paymentControlPluginNames;
private Payment result;
+ private BigDecimal processedAmount;
+ private Currency processedCurrency;
- public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames, final boolean isApiPayment, @Nullable final UUID paymentId, final String paymentExternalKey,
- @Nullable final String paymentTransactionExternalKey, final TransactionType transactionType,
- final Account account, @Nullable final UUID paymentMethodId, final BigDecimal amount, final Currency currency,
+ public PaymentStateControlContext(@Nullable final List<String> paymentControlPluginNames,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ @Nullable final UUID paymentId,
+ final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ @Nullable final String paymentTransactionExternalKey,
+ final TransactionType transactionType,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ final BigDecimal amount,
+ final Currency currency,
final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext, final CallContext callContext) {
- super(isApiPayment, paymentId, null, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
+ super(isApiPayment, paymentId, transactionId, null, paymentExternalKey, paymentTransactionExternalKey, transactionType, account, paymentMethodId, amount, currency, true, null, properties, internalCallContext, callContext);
this.paymentControlPluginNames = paymentControlPluginNames;
+ this.isSuccess = isSuccess;
}
public DateTime getRetryDate() {
@@ -72,6 +86,26 @@ public class PaymentStateControlContext extends PaymentStateContext {
this.result = result;
}
+ public Boolean isSuccess() {
+ return isSuccess;
+ }
+
+ public BigDecimal getProcessedAmount() {
+ return processedAmount;
+ }
+
+ public void setProcessedAmount(final BigDecimal processedAmount) {
+ this.processedAmount = processedAmount;
+ }
+
+ public Currency getProcessedCurrency() {
+ return processedCurrency;
+ }
+
+ public void setProcessedCurrency(final Currency processedCurrency) {
+ this.processedCurrency = processedCurrency;
+ }
+
public PaymentTransaction getCurrentTransaction() {
if (result == null || result.getTransactions() == null) {
return null;
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
index 39060ad..3594cfc 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/OperationCallbackBase.java
@@ -64,7 +64,7 @@ public abstract class OperationCallbackBase<CallbackOperationResult, CallbackOpe
try {
final Callable<PluginDispatcherReturnType<OperationResult>> task = new CallableWithAccountLock<OperationResult, ExceptionType>(locker,
- account.getExternalKey(),
+ account.getId(),
paymentConfig,
callback);
final OperationResult operationResult = PaymentPluginDispatcher.dispatchWithExceptionHandling(account, pluginNames, task, paymentPluginDispatcher);
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
index 975c4b8..d4b2b39 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/sm/PluginControlPaymentAutomatonRunner.java
@@ -54,6 +54,7 @@ import org.killbill.billing.payment.core.sm.control.CreditControlOperation;
import org.killbill.billing.payment.core.sm.control.DefaultControlCompleted;
import org.killbill.billing.payment.core.sm.control.DefaultControlInitiated;
import org.killbill.billing.payment.core.sm.control.NoopControlInitiated;
+import org.killbill.billing.payment.core.sm.control.NotificationOfStateChangeControlOperation;
import org.killbill.billing.payment.core.sm.control.PaymentStateControlContext;
import org.killbill.billing.payment.core.sm.control.PurchaseControlOperation;
import org.killbill.billing.payment.core.sm.control.RefundControlOperation;
@@ -83,7 +84,8 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
CREDIT,
PURCHASE,
REFUND,
- VOID
+ VOID,
+ NOTIFICATION_OF_STATE_CHANGE
}
protected final OSGIServiceRegistration<PaymentControlPluginApi> paymentControlPluginRegistry;
@@ -107,25 +109,138 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
this.paymentConfig = paymentConfig;
}
- public Payment run(final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
+ final CallContext callContext,
+ final InternalCallContext internalCallContext) throws PaymentApiException {
+ return run(paymentControlStateMachineHelper.getInitialState(),
+ isApiPayment,
+ isSuccess,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
+ }
+
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return run(paymentControlStateMachineHelper.getInitialState(), isApiPayment, transactionType, controlOperation, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
- amount, currency, properties, paymentControlPluginNames, callContext, internalCallContext);
+ return run(state,
+ isApiPayment,
+ null,
+ transactionType,
+ controlOperation,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ null,
+ paymentTransactionExternalKey,
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
}
- public Payment run(final State state, final boolean isApiPayment, final TransactionType transactionType, final ControlOperation controlOperation, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
- @Nullable final BigDecimal amount, @Nullable final Currency currency,
- final Iterable<PluginProperty> properties, @Nullable final List<String> paymentControlPluginNames,
+ public Payment run(final State state,
+ final boolean isApiPayment,
+ final Boolean isSuccess,
+ final TransactionType transactionType,
+ final ControlOperation controlOperation,
+ final Account account,
+ @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId,
+ @Nullable final String paymentExternalKey,
+ @Nullable final UUID transactionId,
+ final String paymentTransactionExternalKey,
+ @Nullable final BigDecimal amount,
+ @Nullable final Currency currency,
+ final Iterable<PluginProperty> properties,
+ @Nullable final List<String> paymentControlPluginNames,
final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- final PaymentStateControlContext paymentStateContext = createContext(isApiPayment, transactionType, account, paymentMethodId,
- paymentId, paymentExternalKey,
+ final PaymentStateControlContext paymentStateContext = createContext(isApiPayment,
+ isSuccess,
+ transactionType,
+ account,
+ paymentMethodId,
+ paymentId,
+ paymentExternalKey,
+ transactionId,
paymentTransactionExternalKey,
- amount, currency,
- properties, paymentControlPluginNames, callContext, internalCallContext);
+ amount,
+ currency,
+ properties,
+ paymentControlPluginNames,
+ callContext,
+ internalCallContext);
try {
final OperationCallback callback = createOperationCallback(controlOperation, paymentStateContext);
final LeavingStateCallback leavingStateCallback = new DefaultControlInitiated(this, paymentStateContext, paymentDao, paymentControlStateMachineHelper.getInitialState(), paymentControlStateMachineHelper.getRetriedState(), transactionType);
@@ -159,23 +274,26 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
} catch (final MissingEntryException e) {
throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
} catch (final OperationException e) {
- if (e.getCause() == null) {
- throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
- } else if (e.getCause() instanceof PaymentApiException) {
+ if (e.getCause() instanceof PaymentApiException) {
throw (PaymentApiException) e.getCause();
- } else {
- throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, Objects.firstNonNull(e.getMessage(), ""));
+ // If the control plugin tries to pass us back a PaymentApiException we throw it
+ } else if (e.getCause() instanceof PaymentControlApiException && e.getCause().getCause() instanceof PaymentApiException) {
+ throw (PaymentApiException) e.getCause().getCause();
+ } else if (e.getCause() != null || paymentStateContext.getResult() == null) {
+ throw new PaymentApiException(e.getCause(), ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
}
}
+ // If the result is set (and cause is null), that means we created a Payment but the associated transaction status is 'XXX_FAILURE',
+ // we don't throw, and return the failed Payment instead to be consistent with what happens when we don't go through control api.
return paymentStateContext.getResult();
}
@VisibleForTesting
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey,@Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency, final Iterable<PluginProperty> properties,
final List<String> paymentControlPluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
- return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, paymentId, paymentExternalKey, paymentTransactionExternalKey, transactionType, account,
+ return new PaymentStateControlContext(paymentControlPluginNames, isApiPayment, isSuccess, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey, transactionType, account,
paymentMethodId, amount, currency, properties, internalCallContext, callContext);
}
@@ -207,6 +325,9 @@ public class PluginControlPaymentAutomatonRunner extends PaymentAutomatonRunner
case CHARGEBACK_REVERSAL:
callback = new ChargebackReversalControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
break;
+ case NOTIFICATION_OF_STATE_CHANGE:
+ callback = new NotificationOfStateChangeControlOperation(locker, paymentPluginDispatcher, paymentConfig, paymentStateContext, paymentProcessor, controlPluginRunner);
+ break;
default:
throw new IllegalStateException("Unsupported control operation " + controlOperation);
}
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
index 01b15f1..258c365 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/CallableWithRequestData.java
@@ -19,6 +19,7 @@ package org.killbill.billing.payment.dispatcher;
import java.util.Map;
import java.util.Random;
+import java.util.UUID;
import java.util.concurrent.Callable;
import org.apache.shiro.mgt.SecurityManager;
@@ -44,7 +45,12 @@ public class CallableWithRequestData<T> implements Callable<T> {
final Subject subject,
final Map<String, String> mdcContextMap,
final Callable<T> delegate) {
- this.requestData = requestData;
+ if (requestData == null) {
+ // To make locks re-entrant (for the Janitor), we need a request id
+ this.requestData = new RequestData(UUID.randomUUID().toString());
+ } else {
+ this.requestData = requestData;
+ }
this.random = random;
this.securityManager = securityManager;
this.subject = subject;
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index 9b066d3..0a31eee 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -41,6 +41,8 @@ import org.killbill.billing.payment.core.PaymentGatewayProcessor;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
import org.killbill.billing.payment.core.PluginControlPaymentProcessor;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentAttemptTask;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
import org.killbill.billing.payment.core.janitor.Janitor;
import org.killbill.billing.payment.core.sm.PaymentControlStateMachineHelper;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
@@ -127,6 +129,8 @@ public class PaymentModule extends KillBillModule {
}
protected void installProcessors(final PaymentConfig paymentConfig) {
+ bind(IncompletePaymentAttemptTask.class).asEagerSingleton();
+ bind(IncompletePaymentTransactionTask.class).asEagerSingleton();
bind(PaymentProcessor.class).asEagerSingleton();
bind(PluginControlPaymentProcessor.class).asEagerSingleton();
bind(PaymentGatewayProcessor.class).asEagerSingleton();
diff --git a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
index b8bc30f..81185d4 100644
--- a/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
+++ b/payment/src/main/java/org/killbill/billing/payment/invoice/InvoicePaymentControlPluginApi.java
@@ -76,6 +76,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
@@ -176,7 +177,13 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
log.warn("processedCurrency='{}' of invoice paymentId='{}' doesn't match invoice currency='{}', assuming it is a full payment", paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
invoicePaymentAmount = paymentControlContext.getAmount();
}
- log.debug("Notifying invoice of successful paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentControlContext.getTransactionId(), internalContext);
+ // If it's not SUCCESS, it is PENDING
+ final boolean success = paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS;
+ log.debug("Notifying invoice of {} paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", success ? "successful" : "pending", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
+
+ // For PENDING payments, the attempt will be kept as unsuccessful and an InvoicePaymentErrorInternalEvent sent on the bus (e.g. for Overdue)
invoiceApi.recordPaymentAttemptCompletion(invoiceId,
invoicePaymentAmount,
paymentControlContext.getCurrency(),
@@ -184,7 +191,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
paymentControlContext.getPaymentId(),
paymentControlContext.getTransactionExternalKey(),
paymentControlContext.getCreatedDate(),
- true,
+ success,
internalContext);
}
break;
@@ -355,7 +362,7 @@ public final class InvoicePaymentControlPluginApi implements PaymentControlPlugi
// but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
//
invoiceApi.recordPaymentAttemptInit(invoice.getId(),
- BigDecimal.ZERO,
+ MoreObjects.firstNonNull(paymentControlPluginContext.getAmount(), BigDecimal.ZERO),
paymentControlPluginContext.getCurrency(),
paymentControlPluginContext.getCurrency(),
// Likely to be null, but we don't care as we use the transactionExternalKey
diff --git a/payment/src/main/resources/org/killbill/billing/payment/migration/V20161130055443__payment_external_keys_not_null.sql b/payment/src/main/resources/org/killbill/billing/payment/migration/V20161130055443__payment_external_keys_not_null.sql
new file mode 100644
index 0000000..03b1a30
--- /dev/null
+++ b/payment/src/main/resources/org/killbill/billing/payment/migration/V20161130055443__payment_external_keys_not_null.sql
@@ -0,0 +1,4 @@
+alter table payment_attempts modify payment_external_key varchar(255) not null;
+alter table payment_attempts modify transaction_external_key varchar(255) not null;
+alter table payment_attempt_history modify payment_external_key varchar(255) not null;
+alter table payment_attempt_history modify transaction_external_key varchar(255) not null;
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
index 03f6d8a..e710d8b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestDefaultAdminPaymentApi.java
@@ -206,4 +206,98 @@ public class TestDefaultAdminPaymentApi extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorCode(), "error-code");
Assert.assertEquals(refreshedPaymentTransactionModelDao.getGatewayErrorMsg(), "error-msg");
}
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill-adyen-plugin/pull/60")
+ public void testFixPaymentTransactionStateRefundSuccessToRefundFailed() throws PaymentApiException {
+ final Payment payment = paymentApi.createPurchase(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final Payment refund = paymentApi.createRefund(account,
+ payment.getId(),
+ payment.getPurchasedAmount(),
+ payment.getCurrency(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao2 = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao2 = paymentDao.getPaymentTransaction(refund.getTransactions().get(1).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao2.getStateName(), "REFUND_SUCCESS");
+ Assert.assertEquals(paymentModelDao2.getLastSuccessStateName(), "REFUND_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao2.getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ adminPaymentApi.fixPaymentTransactionState(refund,
+ refund.getTransactions().get(1),
+ TransactionStatus.PAYMENT_FAILURE,
+ null, /* Let Kill Bill figure it out */
+ null, /* Let Kill Bill figure it out */
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao3 = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao3 = paymentDao.getPaymentTransaction(refund.getTransactions().get(1).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao3.getStateName(), "REFUND_FAILED");
+ Assert.assertEquals(paymentModelDao3.getLastSuccessStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao3.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill-adyen-plugin/pull/60")
+ public void testFixPaymentTransactionStateRefundFailedToRefundSuccess() throws PaymentApiException {
+ final Payment payment = paymentApi.createPurchase(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(payment.getTransactions().get(0).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao.getStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentModelDao.getLastSuccessStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao.getTransactionStatus(), TransactionStatus.SUCCESS);
+
+ final Payment refund = paymentApi.createRefund(account,
+ payment.getId(),
+ payment.getPurchasedAmount(),
+ payment.getCurrency(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.ERROR.toString(), false)),
+ callContext);
+
+ final PaymentModelDao paymentModelDao2 = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao2 = paymentDao.getPaymentTransaction(refund.getTransactions().get(1).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao2.getStateName(), "REFUND_FAILED");
+ Assert.assertEquals(paymentModelDao2.getLastSuccessStateName(), "PURCHASE_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao2.getTransactionStatus(), TransactionStatus.PAYMENT_FAILURE);
+
+ adminPaymentApi.fixPaymentTransactionState(refund,
+ refund.getTransactions().get(1),
+ TransactionStatus.SUCCESS,
+ null, /* Let Kill Bill figure it out */
+ null, /* Let Kill Bill figure it out */
+ ImmutableList.<PluginProperty>of(),
+ callContext);
+
+ final PaymentModelDao paymentModelDao3 = paymentDao.getPayment(payment.getId(), internalCallContext);
+ final PaymentTransactionModelDao paymentTransactionModelDao3 = paymentDao.getPaymentTransaction(refund.getTransactions().get(1).getId(), internalCallContext);
+ Assert.assertEquals(paymentModelDao3.getStateName(), "REFUND_SUCCESS");
+ Assert.assertEquals(paymentModelDao3.getLastSuccessStateName(), "REFUND_SUCCESS");
+ Assert.assertEquals(paymentTransactionModelDao3.getTransactionStatus(), TransactionStatus.SUCCESS);
+ }
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
index 15d8dac..4ad4a59 100644
--- a/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
+++ b/payment/src/test/java/org/killbill/billing/payment/api/TestPaymentApiWithControl.java
@@ -38,10 +38,7 @@ import org.killbill.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
import org.killbill.billing.payment.retry.DefaultFailureCallResult;
import org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult;
-import org.killbill.commons.request.Request;
-import org.killbill.commons.request.RequestData;
import org.testng.Assert;
-import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -91,14 +88,6 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
}
},
testPaymentControlPluginApi);
-
- // Required for re-entrant locks to work
- Request.setPerThreadRequestData(new RequestData(UUID.randomUUID().toString()));
- }
-
- @AfterMethod(groups = "slow")
- public void tearDown() throws Exception {
- Request.resetPerThreadRequestData();
}
// Verify Payment control API can be used to change the paymentMethodId on the fly and this is reflected in the created Payment.
@@ -111,6 +100,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
final Payment payment = paymentApi.createAuthorizationWithPaymentControl(account, account.getPaymentMethodId(), null, BigDecimal.TEN, Currency.USD, UUID.randomUUID().toString(),
UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getPaymentMethodId(), newPaymentMethodId);
+
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.TEN,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -126,6 +122,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -133,6 +136,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -148,6 +158,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createAuthorizationWithPaymentControl(account, payment.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
@@ -161,6 +178,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -187,6 +211,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
ImmutableList.<PluginProperty>of(), PAYMENT_OPTIONS, callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -196,6 +227,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -222,6 +260,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnFailureForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createCaptureWithPaymentControl(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey,
pendingPluginProperties, PAYMENT_OPTIONS, callContext);
@@ -236,6 +281,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -251,6 +303,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
@@ -273,6 +332,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createAuthorization(account, account.getPaymentMethodId(), payment.getId(), requestedAmount, payment.getCurrency(), payment.getExternalKey(),
payment.getTransactions().get(0).getExternalKey(), ImmutableList.<PluginProperty>of(), callContext);
@@ -312,6 +378,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.PENDING);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
@@ -346,6 +419,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+ verifyOnFailureForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+
try {
payment = paymentApi.createCapture(account, payment.getId(), requestedAmount, payment.getCurrency(), paymentTransactionExternalKey, ImmutableList.<PluginProperty>of(), callContext);
Assert.fail();
@@ -381,6 +461,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -409,8 +496,14 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(0).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(0).getExternalKey(), paymentTransactionExternalKey);
- }
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
+ }
@Test(groups = "slow")
public void testCreateAuthSuccessCapturePendingNoControlCompleteWithControl() throws PaymentApiException {
@@ -444,6 +537,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.SUCCESS);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -483,6 +583,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
Assert.assertEquals(payment.getTransactions().get(1).getTransactionStatus(), TransactionStatus.UNKNOWN);
Assert.assertEquals(payment.getTransactions().get(1).getExternalKey(), paymentTransactionExternalKey);
+
+ verifyPriorAndOnFailure(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ BigDecimal.ZERO,
+ Currency.USD);
}
@Test(groups = "slow")
@@ -496,6 +603,13 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 1);
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
+ verifyOnSuccess(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(0).getId(),
+ payment.getTransactions().get(0).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+
payment = paymentApi.createCapture(account, payment.getId(), payment.getAuthAmount(), payment.getCurrency(), UUID.randomUUID().toString(), ImmutableList.<PluginProperty>of(), callContext);
Assert.assertEquals(payment.getAuthAmount().compareTo(requestedAmount), 0);
Assert.assertEquals(payment.getCapturedAmount().compareTo(requestedAmount), 0);
@@ -522,6 +636,193 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
Assert.assertEquals(payment.getTransactions().size(), 2);
Assert.assertNull(((DefaultPaymentTransaction) payment.getTransactions().get(0)).getAttemptId());
Assert.assertNotNull(((DefaultPaymentTransaction) payment.getTransactions().get(1)).getAttemptId());
+
+ verifyOnSuccessForFollowOnTransaction(payment.getId(),
+ payment.getExternalKey(),
+ payment.getTransactions().get(1).getId(),
+ payment.getTransactions().get(1).getExternalKey(),
+ requestedAmount,
+ Currency.USD);
+ }
+
+ private void verifyPriorAndOnSuccess(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnSuccess(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallPaymentId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnSuccessForFollowOnTransaction(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency());
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyPriorAndOnFailure(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency(), processedCurrency);
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnFailure(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallPaymentId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
+ }
+
+ private void verifyOnFailureForFollowOnTransaction(final UUID paymentId,
+ final String paymentExternalKey,
+ final UUID paymentTransactionId,
+ final String paymentTransactionExternalKey,
+ final BigDecimal processAmount,
+ final Currency processedCurrency) {
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallTransactionId());
+ Assert.assertEquals(testPaymentControlPluginApi.getActualPriorCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualPriorCallProcessedCurrency());
+
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallPaymentExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionId());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallTransactionExternalKey());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedAmount());
+ Assert.assertNull(testPaymentControlPluginApi.getActualOnSuccessCallProcessedCurrency());
+
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentId(), paymentId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallPaymentExternalKey(), paymentExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionId(), paymentTransactionId);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallTransactionExternalKey(), paymentTransactionExternalKey);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedAmount().compareTo(processAmount), 0);
+ Assert.assertEquals(testPaymentControlPluginApi.getActualOnFailureCallProcessedCurrency(), processedCurrency);
+
+ testPaymentControlPluginApi.resetActualValues();
}
public static class TestPaymentControlPluginApi implements PaymentControlPluginApi {
@@ -530,12 +831,112 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
private UUID newPaymentMethodId;
+ private UUID actualPriorCallPaymentId;
+ private String actualPriorCallPaymentExternalKey;
+ private UUID actualPriorCallTransactionId;
+ private String actualPriorCallTransactionExternalKey;
+ private BigDecimal actualPriorCallProcessedAmount;
+ private Currency actualPriorCallProcessedCurrency;
+
+ private UUID actualOnSuccessCallPaymentId;
+ private String actualOnSuccessCallPaymentExternalKey;
+ private UUID actualOnSuccessCallTransactionId;
+ private String actualOnSuccessCallTransactionExternalKey;
+ private BigDecimal actualOnSuccessCallProcessedAmount;
+ private Currency actualOnSuccessCallProcessedCurrency;
+
+ private UUID actualOnFailureCallPaymentId;
+ private String actualOnFailureCallPaymentExternalKey;
+ private UUID actualOnFailureCallTransactionId;
+ private String actualOnFailureCallTransactionExternalKey;
+ private BigDecimal actualOnFailureCallProcessedAmount;
+ private Currency actualOnFailureCallProcessedCurrency;
+
public void setNewPaymentMethodId(final UUID newPaymentMethodId) {
this.newPaymentMethodId = newPaymentMethodId;
}
+ public UUID getActualPriorCallPaymentId() {
+ return actualPriorCallPaymentId;
+ }
+
+ public String getActualPriorCallPaymentExternalKey() {
+ return actualPriorCallPaymentExternalKey;
+ }
+
+ public UUID getActualPriorCallTransactionId() {
+ return actualPriorCallTransactionId;
+ }
+
+ public String getActualPriorCallTransactionExternalKey() {
+ return actualPriorCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualPriorCallProcessedAmount() {
+ return actualPriorCallProcessedAmount;
+ }
+
+ public Currency getActualPriorCallProcessedCurrency() {
+ return actualPriorCallProcessedCurrency;
+ }
+
+ public UUID getActualOnSuccessCallPaymentId() {
+ return actualOnSuccessCallPaymentId;
+ }
+
+ public String getActualOnSuccessCallPaymentExternalKey() {
+ return actualOnSuccessCallPaymentExternalKey;
+ }
+
+ public UUID getActualOnSuccessCallTransactionId() {
+ return actualOnSuccessCallTransactionId;
+ }
+
+ public String getActualOnSuccessCallTransactionExternalKey() {
+ return actualOnSuccessCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualOnSuccessCallProcessedAmount() {
+ return actualOnSuccessCallProcessedAmount;
+ }
+
+ public Currency getActualOnSuccessCallProcessedCurrency() {
+ return actualOnSuccessCallProcessedCurrency;
+ }
+
+ public UUID getActualOnFailureCallPaymentId() {
+ return actualOnFailureCallPaymentId;
+ }
+
+ public String getActualOnFailureCallPaymentExternalKey() {
+ return actualOnFailureCallPaymentExternalKey;
+ }
+
+ public UUID getActualOnFailureCallTransactionId() {
+ return actualOnFailureCallTransactionId;
+ }
+
+ public String getActualOnFailureCallTransactionExternalKey() {
+ return actualOnFailureCallTransactionExternalKey;
+ }
+
+ public BigDecimal getActualOnFailureCallProcessedAmount() {
+ return actualOnFailureCallProcessedAmount;
+ }
+
+ public Currency getActualOnFailureCallProcessedCurrency() {
+ return actualOnFailureCallProcessedCurrency;
+ }
+
@Override
public PriorPaymentControlResult priorCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualPriorCallPaymentId = context.getPaymentId();
+ actualPriorCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualPriorCallTransactionId = context.getTransactionId();
+ actualPriorCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualPriorCallProcessedAmount = context.getProcessedAmount();
+ actualPriorCallProcessedCurrency = context.getProcessedCurrency();
+
return new PriorPaymentControlResult() {
@Override
public boolean isAborted() {
@@ -566,12 +967,49 @@ public class TestPaymentApiWithControl extends PaymentTestSuiteWithEmbeddedDB {
@Override
public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualOnSuccessCallPaymentId = context.getPaymentId();
+ actualOnSuccessCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualOnSuccessCallTransactionId = context.getTransactionId();
+ actualOnSuccessCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualOnSuccessCallProcessedAmount = context.getProcessedAmount();
+ actualOnSuccessCallProcessedCurrency = context.getProcessedCurrency();
+
return new DefaultOnSuccessPaymentControlResult();
}
@Override
public OnFailurePaymentControlResult onFailureCall(final PaymentControlContext context, final Iterable<PluginProperty> properties) throws PaymentControlApiException {
+ actualOnFailureCallPaymentId = context.getPaymentId();
+ actualOnFailureCallPaymentExternalKey = context.getPaymentExternalKey();
+ actualOnFailureCallTransactionId = context.getTransactionId();
+ actualOnFailureCallTransactionExternalKey = context.getTransactionExternalKey();
+ actualOnFailureCallProcessedAmount = context.getProcessedAmount();
+ actualOnFailureCallProcessedCurrency = context.getProcessedCurrency();
+
return new DefaultFailureCallResult(null);
}
+
+ public void resetActualValues() {
+ actualPriorCallPaymentId = null;
+ actualPriorCallPaymentExternalKey = null;
+ actualPriorCallTransactionId = null;
+ actualPriorCallTransactionExternalKey = null;
+ actualPriorCallProcessedAmount = null;
+ actualPriorCallProcessedCurrency = null;
+
+ actualOnSuccessCallPaymentId = null;
+ actualOnSuccessCallPaymentExternalKey = null;
+ actualOnSuccessCallTransactionId = null;
+ actualOnSuccessCallTransactionExternalKey = null;
+ actualOnSuccessCallProcessedAmount = null;
+ actualOnSuccessCallProcessedCurrency = null;
+
+ actualOnFailureCallPaymentId = null;
+ actualOnFailureCallPaymentExternalKey = null;
+ actualOnFailureCallTransactionId = null;
+ actualOnFailureCallTransactionExternalKey = null;
+ actualOnFailureCallProcessedAmount = null;
+ actualOnFailureCallProcessedCurrency = null;
+ }
}
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
new file mode 100644
index 0000000..5a137ac
--- /dev/null
+++ b/payment/src/test/java/org/killbill/billing/payment/core/janitor/TestIncompletePaymentTransactionTaskWithDB.java
@@ -0,0 +1,106 @@
+/*
+ * 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.payment.core.janitor;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.UUID;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.payment.PaymentTestSuiteWithEmbeddedDB;
+import org.killbill.billing.payment.api.Payment;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.plugin.api.PaymentPluginStatus;
+import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.util.globallocker.LockerType;
+import org.killbill.commons.locker.GlobalLock;
+import org.killbill.commons.locker.LockFailedException;
+import org.killbill.notificationq.api.NotificationEvent;
+import org.killbill.notificationq.api.NotificationEventWithMetadata;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestIncompletePaymentTransactionTaskWithDB extends PaymentTestSuiteWithEmbeddedDB {
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+ private Account account;
+
+ @BeforeClass(groups = "slow")
+ protected void beforeClass() throws Exception {
+ super.beforeClass();
+
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
+ }
+
+ @BeforeMethod(groups = "slow")
+ public void beforeMethod() throws Exception {
+ super.beforeMethod();
+
+ mockPaymentProviderPlugin.clear();
+ account = testHelper.createTestAccount(UUID.randomUUID().toString(), true);
+ }
+
+ @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/675")
+ public void testHandleLockExceptions() throws PaymentApiException {
+ final Payment payment = paymentApi.createPurchase(account,
+ account.getPaymentMethodId(),
+ null,
+ BigDecimal.TEN,
+ Currency.EUR,
+ UUID.randomUUID().toString(),
+ UUID.randomUUID().toString(),
+ ImmutableList.<PluginProperty>of(new PluginProperty(MockPaymentProviderPlugin.PLUGIN_PROPERTY_PAYMENT_PLUGIN_STATUS_OVERRIDE, PaymentPluginStatus.PENDING.toString(), false)),
+ callContext);
+
+ final UUID transactionId = payment.getTransactions().get(0).getId();
+ final JanitorNotificationKey notificationKey = new JanitorNotificationKey(transactionId, incompletePaymentTransactionTask.getClass().toString(), 1);
+ final UUID userToken = UUID.randomUUID();
+
+ Assert.assertTrue(incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId()).isEmpty());
+
+ GlobalLock lock = null;
+ try {
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), paymentConfig.getMaxGlobalLockRetries());
+
+ incompletePaymentTransactionTask.processNotification(notificationKey, userToken, internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+
+ final List<NotificationEventWithMetadata<NotificationEvent>> futureNotifications = incompletePaymentTransactionTask.janitorQueue.getFutureNotificationForSearchKeys(internalCallContext.getAccountRecordId(), internalCallContext.getTenantRecordId());
+ Assert.assertFalse(futureNotifications.isEmpty());
+ Assert.assertEquals(futureNotifications.get(0).getUserToken(), userToken);
+ Assert.assertEquals(futureNotifications.get(0).getEvent().getClass(), JanitorNotificationKey.class);
+ final JanitorNotificationKey event = (JanitorNotificationKey) futureNotifications.get(0).getEvent();
+ Assert.assertEquals(event.getUuidKey(), transactionId);
+ Assert.assertEquals((int) event.getAttemptNumber(), 2);
+
+ // Based on config "15s,1m,3m,1h,1d,1d,1d,1d,1d"
+ Assert.assertTrue(futureNotifications.get(0).getEffectiveDate().compareTo(clock.getUTCNow().plusMinutes(1).plusSeconds(1)) < 0);
+ } catch (final LockFailedException e) {
+ Assert.fail();
+ } finally {
+ if (lock != null) {
+ lock.release();
+ }
+ }
+ }
+}
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
index e48f3b4..80cb640 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/control/TestControlPluginRunner.java
@@ -43,6 +43,7 @@ public class TestControlPluginRunner extends PaymentTestSuiteNoDB {
final UUID paymentMethodId = UUIDs.randomUUID();
final UUID paymentId = UUIDs.randomUUID();
final String paymentExternalKey = UUIDs.randomUUID().toString();
+ final UUID paymentTransactionId = UUIDs.randomUUID();
final String paymentTransactionExternalKey = UUIDs.randomUUID().toString();
final BigDecimal amount = BigDecimal.ONE;
final Currency currency = Currency.USD;
@@ -55,12 +56,15 @@ public class TestControlPluginRunner extends PaymentTestSuiteNoDB {
null,
paymentId,
paymentExternalKey,
+ paymentTransactionId,
paymentTransactionExternalKey,
PaymentApiType.PAYMENT_TRANSACTION,
TransactionType.AUTHORIZE,
null,
amount,
currency,
+ null,
+ null,
true,
paymentControlPluginNames,
pluginProperties,
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
index 386ed00..24ae2a7 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/MockRetryablePaymentAutomatonRunner.java
@@ -74,13 +74,13 @@ public class MockRetryablePaymentAutomatonRunner extends PluginControlPaymentAut
}
@Override
- PaymentStateControlContext createContext(final boolean isApiPayment, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
- @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, final String paymentTransactionExternalKey,
+ PaymentStateControlContext createContext(final boolean isApiPayment, final Boolean isSuccess, final TransactionType transactionType, final Account account, @Nullable final UUID paymentMethodId,
+ @Nullable final UUID paymentId, @Nullable final String paymentExternalKey, @Nullable final UUID transactionId, final String paymentTransactionExternalKey,
@Nullable final BigDecimal amount, @Nullable final Currency currency,
final Iterable<PluginProperty> properties,
final List<String> pluginNames, final CallContext callContext, final InternalCallContext internalCallContext) throws PaymentApiException {
if (context == null) {
- return super.createContext(isApiPayment, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, paymentTransactionExternalKey,
+ return super.createContext(isApiPayment, isSuccess, transactionType, account, paymentMethodId, paymentId, paymentExternalKey, transactionId, paymentTransactionExternalKey,
amount, currency, properties, pluginNames, callContext, internalCallContext);
} else {
return context;
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
index e34c632..405d5be 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestPluginOperation.java
@@ -65,7 +65,7 @@ public class TestPluginOperation extends PaymentTestSuiteNoDB {
@BeforeMethod(groups = "fast")
public void beforeMethod() throws Exception {
super.beforeMethod();
- Mockito.when(account.getExternalKey()).thenReturn(UUID.randomUUID().toString());
+ Mockito.when(account.getId()).thenReturn(UUID.randomUUID());
}
@Test(groups = "fast")
diff --git a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
index 4f3b429..752f40c 100644
--- a/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
+++ b/payment/src/test/java/org/killbill/billing/payment/core/sm/TestRetryablePayment.java
@@ -177,7 +177,9 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
new PaymentStateControlContext(ImmutableList.<String>of(MockPaymentControlProviderPlugin.PLUGIN_NAME),
true,
null,
+ null,
paymentExternalKey,
+ null,
paymentTransactionExternalKey,
TransactionType.AUTHORIZE,
account,
@@ -720,7 +722,7 @@ public class TestRetryablePayment extends PaymentTestSuiteNoDB {
GlobalLock lock = null;
try {
// Grab lock so that operation later will fail...
- lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getExternalKey(), 1);
+ lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), account.getId().toString(), 1);
mockRetryProviderPlugin
.setAborted(false)
diff --git a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
index 73cc4ea..061bbc5 100644
--- a/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
+++ b/payment/src/test/java/org/killbill/billing/payment/PaymentTestSuiteWithEmbeddedDB.java
@@ -30,16 +30,20 @@ import org.killbill.billing.payment.caching.StateMachineConfigCache;
import org.killbill.billing.payment.core.PaymentExecutors;
import org.killbill.billing.payment.core.PaymentMethodProcessor;
import org.killbill.billing.payment.core.PaymentProcessor;
+import org.killbill.billing.payment.core.janitor.IncompletePaymentTransactionTask;
+import org.killbill.billing.payment.core.janitor.Janitor;
import org.killbill.billing.payment.core.sm.PaymentStateMachineHelper;
import org.killbill.billing.payment.dao.PaymentDao;
import org.killbill.billing.payment.glue.PaymentModule;
import org.killbill.billing.payment.glue.TestPaymentModuleWithEmbeddedDB;
import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
import org.killbill.billing.payment.provider.MockPaymentProviderPlugin;
+import org.killbill.billing.payment.retry.DefaultRetryService;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.config.definition.PaymentConfig;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.locker.GlobalLocker;
import org.killbill.commons.profiling.Profiling;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
@@ -61,6 +65,8 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
@Inject
protected PaymentProcessor paymentProcessor;
@Inject
+ protected DefaultRetryService retryService;
+ @Inject
protected InvoiceInternalApi invoiceApi;
@Inject
protected OSGIServiceRegistration<PaymentPluginApi> registry;
@@ -88,6 +94,12 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
protected NonEntityDao nonEntityDao;
@Inject
protected StateMachineConfigCache stateMachineConfigCache;
+ @Inject
+ protected Janitor janitor;
+ @Inject
+ protected IncompletePaymentTransactionTask incompletePaymentTransactionTask;
+ @Inject
+ protected GlobalLocker locker;
@Override
protected KillbillConfigSource getConfigSource() {
@@ -113,10 +125,14 @@ public abstract class PaymentTestSuiteWithEmbeddedDB extends GuicyKillbillTestSu
eventBus.start();
Profiling.resetPerThreadProfilingData();
clock.resetDeltaFromReality();
+
+ janitor.initialize();
+ janitor.start();
}
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
+ janitor.stop();
eventBus.stop();
paymentExecutors.stop();
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 1f81aeb..b86f476 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -73,6 +73,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
private final AtomicBoolean makeNextPaymentFailWithCancellation = new AtomicBoolean(false);
private final AtomicBoolean makeNextPaymentFailWithException = new AtomicBoolean(false);
private final AtomicBoolean makeAllPaymentsFailWithError = new AtomicBoolean(false);
+ private final AtomicBoolean makeNextPaymentPending = new AtomicBoolean(false);
private final AtomicInteger makePluginWaitSomeMilliseconds = new AtomicInteger(0);
private final AtomicReference<BigDecimal> overrideNextProcessedAmount = new AtomicReference<BigDecimal>();
private final AtomicReference<Currency> overrideNextProcessedCurrency = new AtomicReference<Currency>();
@@ -200,6 +201,7 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeAllPaymentsFailWithError.set(false);
makeNextPaymentFailWithError.set(false);
makeNextPaymentFailWithCancellation.set(false);
+ makeNextPaymentPending.set(false);
makePluginWaitSomeMilliseconds.set(0);
overrideNextProcessedAmount.set(null);
paymentMethods.clear();
@@ -212,6 +214,10 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
makeNextPaymentFailWithError.set(true);
}
+ public void makeNextPaymentPending() {
+ makeNextPaymentPending.set(true);
+ }
+
public void makeNextPaymentFailWithCancellation() {
makeNextPaymentFailWithCancellation.set(true);
}
@@ -432,6 +438,8 @@ public class MockPaymentProviderPlugin implements PaymentPluginApi {
status = PaymentPluginStatus.ERROR;
} else if (makeNextPaymentFailWithCancellation.getAndSet(false)) {
status = PaymentPluginStatus.CANCELED;
+ } else if (makeNextPaymentPending.getAndSet(false)) {
+ status = PaymentPluginStatus.PENDING;
} else {
status = PaymentPluginStatus.PROCESSED;
}
diff --git a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
index c248d23..b8d044b 100644
--- a/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
+++ b/payment/src/test/java/org/killbill/billing/payment/TestJanitor.java
@@ -101,8 +101,6 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
@Inject
protected NotificationQueueService notificationQueueService;
@Inject
- private Janitor janitor;
- @Inject
private PaymentBusEventHandler handler;
private MockPaymentProviderPlugin mockPaymentProviderPlugin;
@@ -123,15 +121,13 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
mockPaymentProviderPlugin = (MockPaymentProviderPlugin) registry.getServiceForName(MockPaymentProviderPlugin.PLUGIN_NAME);
}
- @AfterClass(groups = "slow")
- protected void afterClass() throws Exception {
- }
-
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
- janitor.initialize();
- janitor.start();
+
+ retryService.initialize();
+ retryService.start();
+
eventBus.register(handler);
testListener.reset();
eventBus.register(testListener);
@@ -143,9 +139,10 @@ public class TestJanitor extends PaymentTestSuiteWithEmbeddedDB {
@AfterMethod(groups = "slow")
public void afterMethod() throws Exception {
+ retryService.stop();
+
testListener.assertListenerStatus();
- janitor.stop();
eventBus.unregister(handler);
eventBus.unregister(testListener);
super.afterMethod();
pom.xml 4(+2 -2)
diff --git a/pom.xml b/pom.xml
index 03da251..583b16d 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.140.2</version>
+ <version>0.141-SNAPSHOT</version>
</parent>
<artifactId>killbill</artifactId>
- <version>0.17.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<packaging>pom</packaging>
<name>killbill</name>
<description>Library for managing recurring subscriptions and the associated billing</description>
profiles/killbill/pom.xml 2(+1 -1)
diff --git a/profiles/killbill/pom.xml b/profiles/killbill/pom.xml
index dda1488..566614e 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles-killbill</artifactId>
diff --git a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
index 182a482..c8dde49 100644
--- a/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killbill/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,164 +1,256 @@
## Top level keys
# general.notice = This notice should rarely, if ever, be used as everyone will see it
+### 0.18.x series ###
+
+# 0.18.0
+0.18.0.updates =
+0.18.0.notices = This is the latest GA release.
+0.18.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.18.0
+
### 0.17.x series ###
+# 0.17.8
+0.17.8.updates =
+0.17.8.notices = This is the latest dev release.
+0.17.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
+
+# 0.17.7
+0.17.7.updates = 0.17.8
+0.17.7.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.7
+
+# 0.17.6
+0.17.6.updates = 0.17.8
+0.17.6.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.6
+
+# 0.17.5
+0.17.5.updates = 0.17.8
+0.17.5.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.5
+
+# 0.17.4
+0.17.4.updates = 0.17.8
+0.17.4.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.4
+
+# 0.17.3
+0.17.3.updates = 0.17.8
+0.17.3.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.3
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.1
+0.17.1.updates = 0.17.8
+0.17.1.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.1
+
# 0.17.0
-0.17.0.updates =
-0.17.0.notices = This is the latest dev release.
+0.17.0.updates = 0.17.8
+0.17.0.notices = We recommend upgrading to 0.17.8, our latest dev release.
0.17.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
### 0.16.x series ###
+# 0.16.12
+0.16.12.updates =
+0.16.12.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.12.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.12
+
+# 0.16.11
+0.16.11.updates = 0.16.12
+0.16.11.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.11.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.11
+
+# 0.16.10
+0.16.10.updates = 0.16.12
+0.16.10.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.10
+
+# 0.16.9
+0.16.9.updates = 0.16.12
+0.16.9.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.9
+
+# 0.16.8
+0.16.8.updates = 0.16.12
+0.16.8.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.8
+
+# 0.16.7
+0.16.7.updates = 0.16.12
+0.16.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.7
+
# 0.16.6
-0.16.6.updates =
-0.16.6.notices = This is the latest GA release.
+0.16.6.updates = 0.16.12
+0.16.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.6
# 0.16.5
-0.16.5.updates = 0.16.6
-0.16.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.5.updates = 0.16.12
+0.16.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.5
# 0.16.4
-0.16.4.updates = 0.16.6
-0.16.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.4.updates = 0.16.12
+0.16.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.4
# 0.16.3
-0.16.3.updates = 0.16.6
-0.16.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.3.updates = 0.16.12
+0.16.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.3
# 0.16.2
-0.16.2.updates = 0.16.6
-0.16.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.2.updates = 0.16.12
+0.16.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.2
# 0.16.1
-0.16.1.updates = 0.16.6
-0.16.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.1.updates = 0.16.12
+0.16.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.1
# 0.16.0
-0.16.0.updates = 0.16.6
-0.16.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.0.updates = 0.16.12
+0.16.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.0
### 0.15.x series ###
# 0.15.10
0.15.10.updates =
-0.15.10.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.10.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.10
# 0.15.9
0.15.9.updates = 0.15.10
-0.15.9.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.9.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.9
# 0.15.8
0.15.8.updates = 0.15.10
-0.15.8.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.8.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.8
# 0.15.7
0.15.7.updates = 0.15.10
-0.15.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.7
# 0.15.6
0.15.6.updates = 0.15.10
-0.15.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.6
# 0.15.5
0.15.5.updates = 0.15.10
-0.15.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.5
# 0.15.4
0.15.4.updates = 0.15.10
-0.15.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.4
# 0.15.3
0.15.3.updates = 0.15.10
-0.15.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.3
# 0.15.2
0.15.2.updates = 0.15.10
-0.15.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.2
# 0.15.1
0.15.1.updates = 0.15.10
-0.15.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.1
# 0.15.0
0.15.0.updates = 0.15.10
-0.15.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.0
### 0.14.x series ###
# 0.14.1
0.14.1.updates =
-0.14.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.14.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.1
# 0.14.0
0.14.0.updates = 0.14.1
-0.14.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.14.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.0
### 0.13.x series ###
# 0.13.7
0.13.7.updates =
-0.13.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.7
# 0.13.6
0.13.6.updates = 0.13.7
-0.13.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.6
# 0.13.5
0.13.5.updates = 0.13.7
-0.13.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.5
# 0.13.4
0.13.4.updates = 0.13.7
-0.13.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.4
# 0.13.3
0.13.3.updates = 0.13.7
-0.13.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.3
# 0.13.2
0.13.2.updates = 0.13.7
-0.13.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.2
# 0.13.1
0.13.1.updates = 0.13.7
-0.13.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.1
### 0.12.x series ###
# 0.12.1
0.12.1.updates =
-0.12.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.12.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.1
# 0.12.0
0.12.0.updates = 0.12.1
-0.12.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.12.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.0
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
index 54c6495..b0a736a 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestEntitlement.java
@@ -125,7 +125,15 @@ public class TestEntitlement extends TestJaxrsBase {
ProductCategory.BASE, term, true);
// Retrieves with GET
- Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
+ Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
+ Assert.assertNull(objFromJson.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
+
// Equality in java client is not correctly implemented so manually check PriceOverrides section and then reset before equality
objFromJson.setPriceOverrides(null);
entitlementJson.setPriceOverrides(null);
@@ -140,17 +148,31 @@ public class TestEntitlement extends TestJaxrsBase {
// Cancel EOT
killBillClient.cancelSubscription(entitlementJson.getSubscriptionId(), EntitlementActionPolicy.END_OF_TERM,
- BillingActionPolicy.END_OF_TERM, CALL_COMPLETION_TIMEOUT_SEC, createdBy, reason, comment);
+ BillingActionPolicy.END_OF_TERM, CALL_COMPLETION_TIMEOUT_SEC, requestOptions);
// Retrieves to check EndDate
- objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
+ objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
assertNotNull(objFromJson.getCancelledDate());
+ Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
+ Assert.assertNull(objFromJson.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
// Uncancel
- killBillClient.uncancelSubscription(entitlementJson.getSubscriptionId(), createdBy, reason, comment);
+ killBillClient.uncancelSubscription(entitlementJson.getSubscriptionId(), requestOptions);
objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
assertNull(objFromJson.getCancelledDate());
+ Assert.assertEquals(objFromJson.getPriceOverrides().size(), 2);
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(objFromJson.getPriceOverrides().get(0).getRecurringPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
+ Assert.assertNull(objFromJson.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(objFromJson.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
}
@Test(groups = "slow", description = "Can handle non existent subscription")
@@ -227,9 +249,16 @@ public class TestEntitlement extends TestJaxrsBase {
overrides.add(new PhasePriceOverride(null, PhaseType.TRIAL.toString(), BigDecimal.TEN, null, null));
input.setPriceOverrides(overrides);
- final Subscription subscription = killBillClient.createSubscription(input, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, basicRequestOptions());
+ final Subscription subscription = killBillClient.createSubscription(input, null, DEFAULT_WAIT_COMPLETION_TIMEOUT_SEC, requestOptions);
+ Assert.assertEquals(subscription.getPriceOverrides().size(), 2);
+ Assert.assertEquals(subscription.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-1-trial");
+ Assert.assertEquals(subscription.getPriceOverrides().get(0).getFixedPrice().compareTo(BigDecimal.TEN), 0);
+ Assert.assertNull(subscription.getPriceOverrides().get(0).getRecurringPrice());
+ Assert.assertEquals(subscription.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-1-evergreen");
+ Assert.assertNull(subscription.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(subscription.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
- final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL);
+ final List<Invoice> invoices = killBillClient.getInvoicesForAccount(accountJson.getAccountId(), true, false, false, AuditLevel.FULL, requestOptions);
assertEquals(invoices.size(), 1);
assertEquals(invoices.get(0).getAmount().compareTo(BigDecimal.TEN), 0);
}
@@ -363,7 +392,7 @@ public class TestEntitlement extends TestJaxrsBase {
input.setProductCategory(ProductCategory.BASE);
input.setBillingPeriod(BillingPeriod.MONTHLY);
input.setPriceList(PriceListSet.DEFAULT_PRICELIST_NAME);
- final Subscription entitlementJson = killBillClient.createSubscription(input, initialDate.toLocalDate().plusMonths(1), -1, createdBy, reason, comment);
+ final Subscription entitlementJson = killBillClient.createSubscription(input, initialDate.toLocalDate().plusMonths(1), -1, requestOptions);
Assert.assertEquals(entitlementJson.getProductName(), input.getProductName());
Assert.assertEquals(entitlementJson.getProductCategory(), input.getProductCategory());
@@ -371,8 +400,15 @@ public class TestEntitlement extends TestJaxrsBase {
Assert.assertEquals(entitlementJson.getPriceList(), input.getPriceList());
// Retrieves with GET
- final Subscription objFromJson = killBillClient.getSubscription(entitlementJson.getSubscriptionId());
- Assert.assertTrue(objFromJson.equals(entitlementJson));
+ final Subscription subscription = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
+ Assert.assertTrue(subscription.equals(entitlementJson));
+ Assert.assertEquals(subscription.getPriceOverrides().size(), 2);
+ Assert.assertEquals(subscription.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
+ Assert.assertEquals(subscription.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
+ Assert.assertNull(subscription.getPriceOverrides().get(0).getRecurringPrice());
+ Assert.assertEquals(subscription.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
+ Assert.assertNull(subscription.getPriceOverrides().get(1).getFixedPrice());
+ Assert.assertEquals(subscription.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));
}
@Test(groups = "slow", description = "Can create an entitlement with an account with autoPayOff")
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java
index 331fbd6..de45ad7 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestInvoicePayment.java
@@ -108,7 +108,6 @@ public class TestInvoicePayment extends TestJaxrsBase {
verifyInvoice(paymentJson, expectedInvoiceBalance);
}
-
@Test(groups = "slow", description = "Can create a full refund with invoice item adjustment")
public void testRefundWithFullInvoiceItemAdjustment() throws Exception {
final InvoicePayment paymentJson = setupScenarioWithPayment();
@@ -164,6 +163,37 @@ public class TestInvoicePayment extends TestJaxrsBase {
verifyInvoice(paymentJson, expectedInvoiceBalance);
}
+ @Test(groups = "slow", description = "Cannot create invoice item adjustments for more than the refund amount")
+ public void testPartialRefundWithFullInvoiceItemAdjustment() throws Exception {
+ final InvoicePayment paymentJson = setupScenarioWithPayment();
+
+ // Get the individual items for the invoice
+ final Invoice invoice = killBillClient.getInvoice(paymentJson.getTargetInvoiceId(), true, false, requestOptions);
+ final InvoiceItem itemToAdjust = invoice.getItems().get(0);
+
+ // Issue a refund for a fraction of the amount
+ final BigDecimal refundAmount = getFractionOfAmount(itemToAdjust.getAmount());
+ final BigDecimal expectedInvoiceBalance = invoice.getBalance();
+
+ // Post and verify the refund
+ final InvoicePaymentTransaction refund = new InvoicePaymentTransaction();
+ refund.setPaymentId(paymentJson.getPaymentId());
+ refund.setAmount(refundAmount);
+ refund.setIsAdjusted(true);
+ final InvoiceItem adjustment = new InvoiceItem();
+ adjustment.setInvoiceItemId(itemToAdjust.getInvoiceItemId());
+ // Ask for an adjustment for the full amount (bigger than the refund amount)
+ adjustment.setAmount(itemToAdjust.getAmount());
+ refund.setAdjustments(ImmutableList.<InvoiceItem>of(adjustment));
+ final Payment paymentAfterRefundJson = killBillClient.createInvoicePaymentRefund(refund, requestOptions);
+
+ // The refund did go through
+ verifyRefund(paymentJson, paymentAfterRefundJson, refundAmount);
+
+ // But not the adjustments
+ verifyInvoice(paymentJson, expectedInvoiceBalance);
+ }
+
@Test(groups = "slow", description = "Can paginate through all payments and refunds")
public void testPaymentsAndRefundsPagination() throws Exception {
InvoicePayment lastPayment = setupScenarioWithPayment();
profiles/killpay/pom.xml 2(+1 -1)
diff --git a/profiles/killpay/pom.xml b/profiles/killpay/pom.xml
index 87a8077..56d67a4 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles-killpay</artifactId>
diff --git a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
index 182a482..c8dde49 100644
--- a/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
+++ b/profiles/killpay/src/main/resources/update-checker/killbill-server-update-list.properties
@@ -1,164 +1,256 @@
## Top level keys
# general.notice = This notice should rarely, if ever, be used as everyone will see it
+### 0.18.x series ###
+
+# 0.18.0
+0.18.0.updates =
+0.18.0.notices = This is the latest GA release.
+0.18.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.18.0
+
### 0.17.x series ###
+# 0.17.8
+0.17.8.updates =
+0.17.8.notices = This is the latest dev release.
+0.17.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.8
+
+# 0.17.7
+0.17.7.updates = 0.17.8
+0.17.7.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.7
+
+# 0.17.6
+0.17.6.updates = 0.17.8
+0.17.6.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.6
+
+# 0.17.5
+0.17.5.updates = 0.17.8
+0.17.5.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.5
+
+# 0.17.4
+0.17.4.updates = 0.17.8
+0.17.4.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.4
+
+# 0.17.3
+0.17.3.updates = 0.17.8
+0.17.3.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.3
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.2
+0.17.2.updates = 0.17.8
+0.17.2.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.2
+
+# 0.17.1
+0.17.1.updates = 0.17.8
+0.17.1.notices = We recommend upgrading to 0.17.8, our latest dev release.
+0.17.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.1
+
# 0.17.0
-0.17.0.updates =
-0.17.0.notices = This is the latest dev release.
+0.17.0.updates = 0.17.8
+0.17.0.notices = We recommend upgrading to 0.17.8, our latest dev release.
0.17.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.17.0
### 0.16.x series ###
+# 0.16.12
+0.16.12.updates =
+0.16.12.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.12.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.12
+
+# 0.16.11
+0.16.11.updates = 0.16.12
+0.16.11.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.11.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.11
+
+# 0.16.10
+0.16.10.updates = 0.16.12
+0.16.10.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.10
+
+# 0.16.9
+0.16.9.updates = 0.16.12
+0.16.9.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.9
+
+# 0.16.8
+0.16.8.updates = 0.16.12
+0.16.8.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.8
+
+# 0.16.7
+0.16.7.updates = 0.16.12
+0.16.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
+0.16.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.7
+
# 0.16.6
-0.16.6.updates =
-0.16.6.notices = This is the latest GA release.
+0.16.6.updates = 0.16.12
+0.16.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.6
# 0.16.5
-0.16.5.updates = 0.16.6
-0.16.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.5.updates = 0.16.12
+0.16.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.5
# 0.16.4
-0.16.4.updates = 0.16.6
-0.16.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.4.updates = 0.16.12
+0.16.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.4
# 0.16.3
-0.16.3.updates = 0.16.6
-0.16.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.3.updates = 0.16.12
+0.16.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.3
# 0.16.2
-0.16.2.updates = 0.16.6
-0.16.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.2.updates = 0.16.12
+0.16.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.2
# 0.16.1
-0.16.1.updates = 0.16.6
-0.16.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.1.updates = 0.16.12
+0.16.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.1
# 0.16.0
-0.16.0.updates = 0.16.6
-0.16.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.16.0.updates = 0.16.12
+0.16.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.16.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.16.0
### 0.15.x series ###
# 0.15.10
0.15.10.updates =
-0.15.10.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.10.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.10.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.10
# 0.15.9
0.15.9.updates = 0.15.10
-0.15.9.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.9.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.9.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.9
# 0.15.8
0.15.8.updates = 0.15.10
-0.15.8.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.8.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.8.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.8
# 0.15.7
0.15.7.updates = 0.15.10
-0.15.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.7
# 0.15.6
0.15.6.updates = 0.15.10
-0.15.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.6
# 0.15.5
0.15.5.updates = 0.15.10
-0.15.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.5
# 0.15.4
0.15.4.updates = 0.15.10
-0.15.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.4
# 0.15.3
0.15.3.updates = 0.15.10
-0.15.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.3
# 0.15.2
0.15.2.updates = 0.15.10
-0.15.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.2
# 0.15.1
0.15.1.updates = 0.15.10
-0.15.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.1
# 0.15.0
0.15.0.updates = 0.15.10
-0.15.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.15.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.15.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.15.0
### 0.14.x series ###
# 0.14.1
0.14.1.updates =
-0.14.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.14.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.1
# 0.14.0
0.14.0.updates = 0.14.1
-0.14.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.14.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.14.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.14.0
### 0.13.x series ###
# 0.13.7
0.13.7.updates =
-0.13.7.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.7.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.7.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.7
# 0.13.6
0.13.6.updates = 0.13.7
-0.13.6.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.6.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.6.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.6
# 0.13.5
0.13.5.updates = 0.13.7
-0.13.5.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.5.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.5.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.5
# 0.13.4
0.13.4.updates = 0.13.7
-0.13.4.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.4.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.4.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.4
# 0.13.3
0.13.3.updates = 0.13.7
-0.13.3.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.3.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.3.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.3
# 0.13.2
0.13.2.updates = 0.13.7
-0.13.2.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.2.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.2.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.2
# 0.13.1
0.13.1.updates = 0.13.7
-0.13.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.13.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.13.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.13.1
### 0.12.x series ###
# 0.12.1
0.12.1.updates =
-0.12.1.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.1.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.12.1.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.1
# 0.12.0
0.12.0.updates = 0.12.1
-0.12.0.notices = We recommend upgrading to 0.16.6, our latest GA release.
+0.12.0.notices = We recommend upgrading to 0.18.0, our latest GA release.
0.12.0.release-notes = https://github.com/killbill/killbill/releases/tag/killbill-0.12.0
profiles/pom.xml 2(+1 -1)
diff --git a/profiles/pom.xml b/profiles/pom.xml
index 22c7a33..8474f5a 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-profiles</artifactId>
subscription/pom.xml 2(+1 -1)
diff --git a/subscription/pom.xml b/subscription/pom.xml
index 77e04bb..43e27d9 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-subscription</artifactId>
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
index 968b006..ac3b021 100644
--- a/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/ddl.sql
@@ -35,7 +35,7 @@ CREATE TABLE subscriptions (
start_date datetime NOT NULL,
bundle_start_date datetime NOT NULL,
charged_through_date datetime DEFAULT NULL,
- migrated bool NOT NULL,
+ migrated bool NOT NULL default FALSE,
created_by varchar(50) NOT NULL,
created_date datetime NOT NULL,
updated_by varchar(50) NOT NULL,
diff --git a/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20161130055443__bundles_external_key_not_null.sql b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20161130055443__bundles_external_key_not_null.sql
new file mode 100644
index 0000000..87d7fef
--- /dev/null
+++ b/subscription/src/main/resources/org/killbill/billing/subscription/migration/V20161130055443__bundles_external_key_not_null.sql
@@ -0,0 +1 @@
+alter table bundles modify external_key varchar(255) not null;
tenant/pom.xml 2(+1 -1)
diff --git a/tenant/pom.xml b/tenant/pom.xml
index 69fe1bf..82faa89 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-tenant</artifactId>
usage/pom.xml 2(+1 -1)
diff --git a/usage/pom.xml b/usage/pom.xml
index 0b98838..8cade0f 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-usage</artifactId>
diff --git a/usage/src/main/resources/org/killbill/billing/usage/migration/V20161130005717__add_tracking_id_index_502.sql b/usage/src/main/resources/org/killbill/billing/usage/migration/V20161130005717__add_tracking_id_index_502.sql
new file mode 100644
index 0000000..21e952f
--- /dev/null
+++ b/usage/src/main/resources/org/killbill/billing/usage/migration/V20161130005717__add_tracking_id_index_502.sql
@@ -0,0 +1 @@
+CREATE INDEX rolled_up_usage_tracking_id_subscription_id_tenant_record_id ON rolled_up_usage(tracking_id, subscription_id, tenant_record_id);
util/pom.xml 2(+1 -1)
diff --git a/util/pom.xml b/util/pom.xml
index 80b2a42..de7529f 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.9-SNAPSHOT</version>
+ <version>0.18.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>killbill-util</artifactId>
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
index 776621b..b2ac2b0 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/InvoiceConfig.java
@@ -36,6 +36,16 @@ public interface InvoiceConfig extends KillbillConfig {
@Description("Maximum target date to consider when generating an invoice")
int getNumberOfMonthsInFuture(@Param("dummy") final InternalTenantContext tenantContext);
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled();
+
+ @Config("org.killbill.invoice.sanitySafetyBoundEnabled")
+ @Default("true")
+ @Description("Whether internal sanity checks to prevent mis- and double-billing are enabled")
+ boolean isSanitySafetyBoundEnabled(@Param("dummy") final InternalTenantContext tenantContext);
+
@Config("org.killbill.invoice.maxDailyNumberOfItemsSafetyBound")
@Default("15")
@Description("Maximum daily number of invoice items to generate for a subscription id")