killbill-uncached
Changes
analytics/pom.xml 10(+10 -0)
catalog/src/test/resources/WeaponsHire.xml 91(+54 -37)
entitlement/pom.xml 50(+26 -24)
entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java 69(+40 -29)
entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java 57(+45 -12)
entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java 20(+11 -9)
entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionBundleData.java 1(+1 -0)
entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java 25(+16 -9)
entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java 136(+118 -18)
entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java 9(+7 -2)
entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java 82(+44 -38)
entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java 146(+129 -17)
entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java 14(+10 -4)
entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java 50(+50 -0)
entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java 6(+6 -0)
entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java 18(+12 -6)
entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java 5(+3 -2)
entitlement/src/test/resources/testInput.xml 91(+54 -37)
Details
analytics/pom.xml 10(+10 -0)
diff --git a/analytics/pom.xml b/analytics/pom.xml
index 10f94d4..ad46c31 100644
--- a/analytics/pom.xml
+++ b/analytics/pom.xml
@@ -89,6 +89,16 @@
</dependency>
<dependency>
<groupId>com.ning.billing</groupId>
+ <artifactId>killbill-invoice</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
+ <artifactId>killbill-payment</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.ning.billing</groupId>
<artifactId>killbill-util</artifactId>
<scope>test</scope>
</dependency>
diff --git a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
index 1735062..ba9dc2a 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -22,22 +22,22 @@ import com.ning.billing.account.api.AccountApiException;
import com.ning.billing.account.api.AccountChangeNotification;
import com.ning.billing.account.api.AccountCreationNotification;
import com.ning.billing.entitlement.api.user.SubscriptionTransition;
+import com.ning.billing.invoice.api.InvoiceCreationNotification;
+import com.ning.billing.payment.api.PaymentError;
+import com.ning.billing.payment.api.PaymentInfo;
-public class AnalyticsListener
-{
+public class AnalyticsListener {
private final BusinessSubscriptionTransitionRecorder bstRecorder;
private final BusinessAccountRecorder bacRecorder;
@Inject
- public AnalyticsListener(final BusinessSubscriptionTransitionRecorder bstRecorder, final BusinessAccountRecorder bacRecorder)
- {
+ public AnalyticsListener(final BusinessSubscriptionTransitionRecorder bstRecorder, final BusinessAccountRecorder bacRecorder) {
this.bstRecorder = bstRecorder;
this.bacRecorder = bacRecorder;
}
@Subscribe
- public void handleSubscriptionTransitionChange(final SubscriptionTransition event) throws AccountApiException
- {
+ public void handleSubscriptionTransitionChange(final SubscriptionTransition event) throws AccountApiException {
switch (event.getTransitionType()) {
case MIGRATE_ENTITLEMENT:
// TODO do nothing for now
@@ -68,18 +68,31 @@ public class AnalyticsListener
}
@Subscribe
- public void handleAccountCreation(final AccountCreationNotification event)
- {
+ public void handleAccountCreation(final AccountCreationNotification event) {
bacRecorder.accountCreated(event.getData());
}
@Subscribe
- public void handleAccountChange(final AccountChangeNotification event)
- {
+ public void handleAccountChange(final AccountChangeNotification event) {
if (!event.hasChanges()) {
return;
}
bacRecorder.accountUpdated(event.getAccountId(), event.getChangedFields());
}
+
+ @Subscribe
+ public void handleInvoice(final InvoiceCreationNotification event) {
+ bacRecorder.accountUpdated(event.getAccountId());
+ }
+
+ @Subscribe
+ public void handlePaymentInfo(final PaymentInfo paymentInfo) {
+ bacRecorder.accountUpdated(paymentInfo);
+ }
+
+ @Subscribe
+ public void handlePaymentError(final PaymentError paymentError) {
+ // TODO - we can't tie the error back to an account yet
+ }
}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
index f7081c7..6832a2b 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessAccountRecorder.java
@@ -22,57 +22,169 @@ import com.ning.billing.account.api.AccountData;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.account.api.ChangedField;
import com.ning.billing.analytics.dao.BusinessAccountDao;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.api.InvoiceUserApi;
+import com.ning.billing.payment.api.PaymentApi;
+import com.ning.billing.payment.api.PaymentAttempt;
+import com.ning.billing.payment.api.PaymentInfo;
import com.ning.billing.util.tag.Tag;
+import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
-public class BusinessAccountRecorder
-{
+public class BusinessAccountRecorder {
private static final Logger log = LoggerFactory.getLogger(BusinessAccountRecorder.class);
private final BusinessAccountDao dao;
private final AccountUserApi accountApi;
+ private final InvoiceUserApi invoiceUserApi;
+ private final PaymentApi paymentApi;
@Inject
- public BusinessAccountRecorder(final BusinessAccountDao dao, final AccountUserApi accountApi)
- {
+ public BusinessAccountRecorder(final BusinessAccountDao dao, final AccountUserApi accountApi, final InvoiceUserApi invoiceUserApi, final PaymentApi paymentApi) {
this.dao = dao;
this.accountApi = accountApi;
+ this.invoiceUserApi = invoiceUserApi;
+ this.paymentApi = paymentApi;
}
- public void accountCreated(final AccountData data)
- {
+ public void accountCreated(final AccountData data) {
final Account account = accountApi.getAccountByKey(data.getExternalKey());
+ final BusinessAccount bac = createBusinessAccountFromAccount(account);
+ log.info("ACCOUNT CREATION " + bac);
+ dao.createAccount(bac);
+ }
+
+ /**
+ * Notification handler for Account changes
+ *
+ * @param accountId account id changed
+ * @param changedFields list of changed fields
+ */
+ public void accountUpdated(final UUID accountId, final List<ChangedField> changedFields) {
+ // None of the fields updated interest us so far - see DefaultAccountChangeNotification
+ // TODO We'll need notifications for tags changes eventually
+ }
+
+ /**
+ * Notification handler for Payment creations
+ *
+ * @param paymentInfo payment object (from the payment plugin)
+ */
+ public void accountUpdated(final PaymentInfo paymentInfo) {
+ final PaymentAttempt paymentAttempt = paymentApi.getPaymentAttemptForPaymentId(paymentInfo.getPaymentId());
+ if (paymentAttempt == null) {
+ return;
+ }
+
+ final Account account = accountApi.getAccountById(paymentAttempt.getAccountId());
+ if (account == null) {
+ return;
+ }
+
+ accountUpdated(account.getId());
+ }
+
+ /**
+ * Notification handler for Invoice creations
+ *
+ * @param accountId account id associated with the created invoice
+ */
+ public void accountUpdated(final UUID accountId) {
+ final Account account = accountApi.getAccountById(accountId);
+
+ if (account == null) {
+ log.warn("Couldn't find account {}", accountId);
+ return;
+ }
+
+ BusinessAccount bac = dao.getAccount(account.getExternalKey());
+ if (bac == null) {
+ bac = createBusinessAccountFromAccount(account);
+ log.info("ACCOUNT CREATION " + bac);
+ dao.createAccount(bac);
+ } else {
+ updateBusinessAccountFromAccount(account, bac);
+ log.info("ACCOUNT UPDATE " + bac);
+ dao.saveAccount(bac);
+ }
+ }
+
+ private BusinessAccount createBusinessAccountFromAccount(final Account account) {
final List<String> tags = new ArrayList<String>();
for (final Tag tag : account.getTagList()) {
tags.add(tag.getTagDefinitionName());
}
- // TODO Need payment and invoice api to fill most fields
final BusinessAccount bac = new BusinessAccount(
- account.getExternalKey(),
- null, // TODO
- tags,
- null, // TODO
- null, // TODO
- null, // TODO
- null, // TODO
- null, // TODO
- null // TODO
+ account.getExternalKey(),
+ invoiceUserApi.getAccountBalance(account.getId()),
+ tags,
+ // These fields will be updated below
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
);
+ updateBusinessAccountFromAccount(account, bac);
- log.info("ACCOUNT CREATION " + bac);
- dao.createAccount(bac);
+ return bac;
}
- public void accountUpdated(final UUID accountId, final List<ChangedField> changedFields)
- {
- // None of the fields updated interest us so far - see DefaultAccountChangeNotification
- // TODO We'll need notifications for tags changes eventually
+ private void updateBusinessAccountFromAccount(final Account account, final BusinessAccount bac) {
+ DateTime lastInvoiceDate = null;
+ BigDecimal totalInvoiceBalance = BigDecimal.ZERO;
+ String lastPaymentStatus = null;
+ String paymentMethod = null;
+ String creditCardType = null;
+ String billingAddressCountry = null;
+
+ // Retrieve invoices information
+ final List<Invoice> invoices = invoiceUserApi.getInvoicesByAccount(account.getId());
+ if (invoices != null && invoices.size() > 0) {
+ final List<String> invoiceIds = new ArrayList<String>();
+ for (final Invoice invoice : invoices) {
+ invoiceIds.add(invoice.getId().toString());
+ totalInvoiceBalance = totalInvoiceBalance.add(invoice.getBalance());
+
+ if (lastInvoiceDate == null || invoice.getInvoiceDate().isAfter(lastInvoiceDate)) {
+ lastInvoiceDate = invoice.getInvoiceDate();
+ }
+ }
+
+ // Retrieve payments information for these invoices
+ DateTime lastPaymentDate = null;
+ final List<PaymentInfo> payments = paymentApi.getPaymentInfo(invoiceIds);
+ if (payments != null) {
+ for (final PaymentInfo payment : payments) {
+ // Use the last payment method/type/country as the default one for the account
+ if (lastPaymentDate == null || payment.getCreatedDate().isAfter(lastPaymentDate)) {
+ lastPaymentDate = payment.getCreatedDate();
+
+ lastPaymentStatus = payment.getStatus();
+ paymentMethod = payment.getPaymentMethod();
+ creditCardType = payment.getCardType();
+ billingAddressCountry = payment.getCardCountry();
+ }
+ }
+ }
+ }
+
+ bac.setLastPaymentStatus(lastPaymentStatus);
+ bac.setPaymentMethod(paymentMethod);
+ bac.setCreditCardType(creditCardType);
+ bac.setBillingAddressCountry(billingAddressCountry);
+ bac.setLastInvoiceDate(lastInvoiceDate);
+ bac.setTotalInvoiceBalance(totalInvoiceBalance);
+
+ bac.setBalance(invoiceUserApi.getAccountBalance(account.getId()));
}
}
diff --git a/analytics/src/test/java/com/ning/billing/analytics/AnalyticsTestModule.java b/analytics/src/test/java/com/ning/billing/analytics/AnalyticsTestModule.java
index 25496b5..5c7ffae 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/AnalyticsTestModule.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/AnalyticsTestModule.java
@@ -16,6 +16,8 @@
package com.ning.billing.analytics;
+import com.ning.billing.invoice.glue.InvoiceModule;
+import com.ning.billing.payment.setup.PaymentModule;
import org.skife.jdbi.v2.IDBI;
import com.ning.billing.account.glue.AccountModule;
import com.ning.billing.analytics.setup.AnalyticsModule;
@@ -41,6 +43,8 @@ public class AnalyticsTestModule extends AnalyticsModule
install(new CatalogModule());
install(new BusModule());
install(new EntitlementModule());
+ install(new InvoiceModule());
+ install(new PaymentModule());
install(new ClockModule());
install(new TagStoreModule());
install(new NotificationQueueModule());
diff --git a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
index c413f0d..06d954f 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/api/TestAnalyticsService.java
@@ -47,14 +47,23 @@ import com.ning.billing.entitlement.api.user.SubscriptionTransition;
import com.ning.billing.entitlement.api.user.SubscriptionTransitionData;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.user.ApiEventType;
+import com.ning.billing.invoice.api.InvoiceCreationNotification;
+import com.ning.billing.invoice.api.user.DefaultInvoiceCreationNotification;
+import com.ning.billing.invoice.dao.InvoiceDao;
+import com.ning.billing.invoice.model.DefaultInvoice;
+import com.ning.billing.invoice.model.FixedPriceInvoiceItem;
+import com.ning.billing.payment.api.PaymentAttempt;
+import com.ning.billing.payment.api.PaymentInfo;
+import com.ning.billing.payment.dao.PaymentDao;
import com.ning.billing.util.bus.Bus;
-import com.ning.billing.util.tag.DescriptiveTag;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.DefaultClock;
import com.ning.billing.util.tag.DefaultTagDefinition;
+import com.ning.billing.util.tag.DescriptiveTag;
import com.ning.billing.util.tag.Tag;
import com.ning.billing.util.tag.dao.TagDefinitionSqlDao;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -62,21 +71,28 @@ import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import java.io.IOException;
+import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import static org.testng.Assert.fail;
@Guice(modules = AnalyticsTestModule.class)
-public class TestAnalyticsService
-{
+public class TestAnalyticsService {
private static final UUID ID = UUID.randomUUID();
private static final String KEY = "12345";
private static final String ACCOUNT_KEY = "pierre-12345";
+ private static final Currency ACCOUNT_CURRENCY = Currency.EUR;
private static final DefaultTagDefinition TAG_ONE = new DefaultTagDefinition("batch20", "something", "pierre");
private static final DefaultTagDefinition TAG_TWO = new DefaultTagDefinition("awesome", "something", "pierre");
+ private static final BigDecimal INVOICE_AMOUNT = BigDecimal.valueOf(1243.11);
+ private static final String PAYMENT_METHOD = "Paypal";
+ private static final String CARD_COUNTRY = "France";
+
+ private final Clock clock = new DefaultClock();
@Inject
private AccountUserApi accountApi;
@@ -88,6 +104,12 @@ public class TestAnalyticsService
private TagDefinitionSqlDao tagDao;
@Inject
+ private InvoiceDao invoiceDao;
+
+ @Inject
+ private PaymentDao paymentDao;
+
+ @Inject
private DefaultAnalyticsService service;
@Inject
@@ -106,50 +128,54 @@ public class TestAnalyticsService
private BusinessSubscriptionTransition expectedTransition;
private AccountCreationNotification accountCreationNotification;
+ private InvoiceCreationNotification invoiceCreationNotification;
+ private PaymentInfo paymentInfoNotification;
@BeforeClass(alwaysRun = true)
- public void startMysql() throws IOException, ClassNotFoundException, SQLException, EntitlementUserApiException
- {
+ public void startMysql() throws IOException, ClassNotFoundException, SQLException, EntitlementUserApiException {
// Killbill generic setup
setupBusAndMySQL();
tagDao.create(TAG_ONE);
tagDao.create(TAG_TWO);
- final MockAccount account = new MockAccount(UUID.randomUUID(), ACCOUNT_KEY, Currency.USD);
+ final MockAccount account = new MockAccount(UUID.randomUUID(), ACCOUNT_KEY, ACCOUNT_CURRENCY);
try {
- List<Tag> tags = new ArrayList<Tag>();
- tags.add(new DescriptiveTag(TAG_ONE, "pierre", new DateTime(DateTimeZone.UTC)));
- tags.add(new DescriptiveTag(TAG_TWO, "pierre", new DateTime(DateTimeZone.UTC)));
+ final List<Tag> tags = new ArrayList<Tag>();
+ tags.add(new DescriptiveTag(TAG_ONE, "pierre", clock.getUTCNow()));
+ tags.add(new DescriptiveTag(TAG_TWO, "pierre", clock.getUTCNow()));
final Account storedAccount = accountApi.createAccount(account, null, tags);
// Create events for the bus and expected results
createSubscriptionTransitionEvent(storedAccount);
createAccountCreationEvent(storedAccount);
+ createInvoiceAndPaymentCreationEvents(storedAccount);
} catch (Throwable t) {
fail("Initializing accounts failed.", t);
}
}
- private void setupBusAndMySQL() throws IOException
- {
+ private void setupBusAndMySQL() throws IOException {
bus.start();
final String analyticsDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/analytics/ddl.sql"));
final String accountDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/account/ddl.sql"));
final String entitlementDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/entitlement/ddl.sql"));
+ final String invoiceDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/invoice/ddl.sql"));
+ final String paymentDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/payment/ddl.sql"));
final String utilDdl = IOUtils.toString(BusinessSubscriptionTransitionDao.class.getResourceAsStream("/com/ning/billing/util/ddl.sql"));
helper.startMysql();
helper.initDb(analyticsDdl);
helper.initDb(accountDdl);
helper.initDb(entitlementDdl);
+ helper.initDb(invoiceDdl);
+ helper.initDb(paymentDdl);
helper.initDb(utilDdl);
}
- private void createSubscriptionTransitionEvent(final Account account) throws EntitlementUserApiException
- {
+ private void createSubscriptionTransitionEvent(final Account account) throws EntitlementUserApiException {
final SubscriptionBundle bundle = entitlementApi.createBundleForAccount(account.getId(), KEY);
// Verify we correctly initialized the account subsystem
@@ -161,63 +187,86 @@ public class TestAnalyticsService
final Plan plan = new MockPlan("platinum-monthly", product);
final PlanPhase phase = new MockPhase(PhaseType.EVERGREEN, plan, MockDuration.UNLIMITED(), 25.95);
final UUID subscriptionId = UUID.randomUUID();
- final DateTime effectiveTransitionTime = new DateTime(DateTimeZone.UTC);
- final DateTime requestedTransitionTime = new DateTime(DateTimeZone.UTC);
+ final DateTime effectiveTransitionTime = clock.getUTCNow();
+ final DateTime requestedTransitionTime = clock.getUTCNow();
final String priceList = "something";
transition = new SubscriptionTransitionData(
- ID,
- subscriptionId,
- bundle.getId(),
- EntitlementEvent.EventType.API_USER,
- ApiEventType.CREATE,
- requestedTransitionTime,
- effectiveTransitionTime,
- null,
- null,
- null,
- null,
- Subscription.SubscriptionState.ACTIVE,
- plan,
- phase,
- priceList
+ ID,
+ subscriptionId,
+ bundle.getId(),
+ EntitlementEvent.EventType.API_USER,
+ ApiEventType.CREATE,
+ requestedTransitionTime,
+ effectiveTransitionTime,
+ null,
+ null,
+ null,
+ null,
+ Subscription.SubscriptionState.ACTIVE,
+ plan,
+ phase,
+ priceList,
+ true
);
expectedTransition = new BusinessSubscriptionTransition(
- ID,
- KEY,
- ACCOUNT_KEY,
- requestedTransitionTime,
- BusinessSubscriptionEvent.subscriptionCreated(plan),
- null,
- new BusinessSubscription(priceList, plan, phase, Currency.USD, effectiveTransitionTime, Subscription.SubscriptionState.ACTIVE, subscriptionId, bundle.getId())
+ ID,
+ KEY,
+ ACCOUNT_KEY,
+ requestedTransitionTime,
+ BusinessSubscriptionEvent.subscriptionCreated(plan),
+ null,
+ new BusinessSubscription(priceList, plan, phase, ACCOUNT_CURRENCY, effectiveTransitionTime, Subscription.SubscriptionState.ACTIVE, subscriptionId, bundle.getId())
);
}
- private void createAccountCreationEvent(final Account account)
- {
+ private void createAccountCreationEvent(final Account account) {
accountCreationNotification = new DefaultAccountCreationEvent(account);
}
+ private void createInvoiceAndPaymentCreationEvents(final Account account) {
+ final DefaultInvoice invoice = new DefaultInvoice(account.getId(), clock.getUTCNow(), ACCOUNT_CURRENCY, clock);
+ final FixedPriceInvoiceItem invoiceItem = new FixedPriceInvoiceItem(
+ UUID.randomUUID(), invoice.getId(), UUID.randomUUID(), "somePlan", "somePhase", clock.getUTCNow(), clock.getUTCNow().plusDays(1),
+ INVOICE_AMOUNT, ACCOUNT_CURRENCY, clock.getUTCNow(), clock.getUTCNow()
+ );
+ invoice.addInvoiceItem(invoiceItem);
+
+ invoiceDao.create(invoice);
+ Assert.assertEquals(invoiceDao.getInvoicesByAccount(account.getId()).size(), 1);
+ Assert.assertEquals(invoiceDao.getInvoicesByAccount(account.getId()).get(0).getInvoiceItems().size(), 1);
+
+ // It doesn't really matter what the events contain - the listener will go back to the db
+ invoiceCreationNotification = new DefaultInvoiceCreationNotification(invoice.getId(), account.getId(),
+ INVOICE_AMOUNT, ACCOUNT_CURRENCY, clock.getUTCNow());
+
+ paymentInfoNotification = new PaymentInfo.Builder().setPaymentId(UUID.randomUUID().toString()).setPaymentMethod(PAYMENT_METHOD).setCardCountry(CARD_COUNTRY).build();
+ final PaymentAttempt paymentAttempt = new PaymentAttempt(UUID.randomUUID(), invoice.getId(), account.getId(), BigDecimal.TEN,
+ ACCOUNT_CURRENCY, clock.getUTCNow(), clock.getUTCNow(), paymentInfoNotification.getPaymentId(), 1, clock.getUTCNow().plusDays(1));
+ paymentDao.createPaymentAttempt(paymentAttempt);
+ paymentDao.savePaymentInfo(paymentInfoNotification);
+ Assert.assertEquals(paymentDao.getPaymentInfo(Arrays.asList(invoice.getId().toString())).size(), 1);
+ }
+
@AfterClass(alwaysRun = true)
- public void stopMysql()
- {
+ public void stopMysql() {
helper.stopMysql();
}
@Test(groups = "slow")
- public void testRegisterForNotifications() throws Exception
- {
+ public void testRegisterForNotifications() throws Exception {
// Make sure the service has been instantiated
Assert.assertEquals(service.getName(), "analytics-service");
// Test the bus and make sure we can register our service
try {
service.registerForNotifications();
- }
- catch (Throwable t) {
+ } catch (Throwable t) {
Assert.fail("Unable to start the bus or service! " + t);
}
+ Assert.assertNull(accountDao.getAccount(ACCOUNT_KEY));
+
// Send events and wait for the async part...
bus.post(transition);
bus.post(accountCreationNotification);
@@ -231,11 +280,24 @@ public class TestAnalyticsService
Assert.assertTrue(accountDao.getAccount(ACCOUNT_KEY).getTags().indexOf(TAG_ONE.getName()) != -1);
Assert.assertTrue(accountDao.getAccount(ACCOUNT_KEY).getTags().indexOf(TAG_TWO.getName()) != -1);
+ // Test invoice integration - the account creation notification has triggered a BAC update
+ Assert.assertTrue(accountDao.getAccount(ACCOUNT_KEY).getTotalInvoiceBalance().compareTo(INVOICE_AMOUNT) == 0);
+
+ // Post the same invoice event again - the invoice balance shouldn't change
+ bus.post(invoiceCreationNotification);
+ Thread.sleep(1000);
+ Assert.assertTrue(accountDao.getAccount(ACCOUNT_KEY).getTotalInvoiceBalance().compareTo(INVOICE_AMOUNT) == 0);
+
+ // Test payment integration - the fields have already been populated, just make sure the code is exercised
+ bus.post(paymentInfoNotification);
+ Thread.sleep(1000);
+ Assert.assertEquals(accountDao.getAccount(ACCOUNT_KEY).getPaymentMethod(), PAYMENT_METHOD);
+ Assert.assertEquals(accountDao.getAccount(ACCOUNT_KEY).getBillingAddressCountry(), CARD_COUNTRY);
+
// Test the shutdown sequence
try {
bus.stop();
- }
- catch (Throwable t) {
+ } catch (Throwable t) {
Assert.fail("Unable to stop the bus!");
}
}
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
index f592c41..6bc4d7a 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockSubscription.java
@@ -19,6 +19,7 @@ package com.ning.billing.analytics;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -146,11 +147,16 @@ public class MockSubscription implements Subscription
@Override
public DateTime getPaidThroughDate() {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException();
}
@Override
public SubscriptionTransition getPreviousTransition() {
return null;
}
+
+ @Override
+ public ProductCategory getCategory() {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
index a9a8293..0c3cf62 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestAnalyticsListener.java
@@ -175,7 +175,8 @@ public class TestAnalyticsListener
nextState,
plan,
phase,
- priceList
+ priceList,
+ true
);
}
@@ -212,7 +213,8 @@ public class TestAnalyticsListener
null,
null,
null,
- null
+ null,
+ true
);
}
@@ -239,7 +241,8 @@ public class TestAnalyticsListener
nextState,
plan,
phase,
- priceList
+ priceList,
+ true
);
}
}
\ No newline at end of file
diff --git a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
index 5c626fc..0765f30 100644
--- a/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
+++ b/api/src/main/java/com/ning/billing/entitlement/api/user/Subscription.java
@@ -19,6 +19,8 @@ package com.ning.billing.entitlement.api.user;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
+
import org.joda.time.DateTime;
import java.util.List;
@@ -69,6 +71,7 @@ public interface Subscription {
public DateTime getPaidThroughDate();
+ public ProductCategory getCategory();
public List<SubscriptionTransition> getActiveTransitions();
diff --git a/api/src/main/java/com/ning/billing/ErrorCode.java b/api/src/main/java/com/ning/billing/ErrorCode.java
index 0ec7b4a..32632a6 100644
--- a/api/src/main/java/com/ning/billing/ErrorCode.java
+++ b/api/src/main/java/com/ning/billing/ErrorCode.java
@@ -37,6 +37,10 @@ public enum ErrorCode {
ENT_CREATE_NO_BUNDLE(1012, "Bundle %s does not exist"),
ENT_CREATE_NO_BP(1013, "Missing Base Subscription for bundle %s"),
ENT_CREATE_BP_EXISTS(1015, "Subscription bundle %s already has a base subscription"),
+ ENT_CREATE_AO_BP_NON_ACTIVE(1017, "Can't create AddOn %s for non active Base Plan"),
+ ENT_CREATE_AO_ALREADY_INCLUDED(1018, "Can't create AddOn %s for BasePlan %s (Already included)"),
+ ENT_CREATE_AO_NOT_AVAILABLE(1019, "Can't create AddOn %s for BasePlan %s (Not available)"),
+
/* Change plan */
ENT_CHANGE_NON_ACTIVE(1021, "Subscription %s is in state %s"),
ENT_CHANGE_FUTURE_CANCELLED(1022, "Subscription %s is future cancelled"),
diff --git a/api/src/main/java/com/ning/billing/payment/api/CreditCardPaymentMethodInfo.java b/api/src/main/java/com/ning/billing/payment/api/CreditCardPaymentMethodInfo.java
index 572e362..bc3d372 100644
--- a/api/src/main/java/com/ning/billing/payment/api/CreditCardPaymentMethodInfo.java
+++ b/api/src/main/java/com/ning/billing/payment/api/CreditCardPaymentMethodInfo.java
@@ -16,8 +16,90 @@
package com.ning.billing.payment.api;
+import org.codehaus.jackson.annotate.JsonCreator;
+import org.codehaus.jackson.annotate.JsonProperty;
+
public final class CreditCardPaymentMethodInfo extends PaymentMethodInfo {
+ private final String cardHolderName;
+ private final String cardType;
+ private final String expirationDate;
+ private final String maskNumber;
+ private final String cardAddress1;
+ private final String cardAddress2;
+ private final String cardCity;
+ private final String cardState;
+ private final String cardPostalCode;
+ private final String cardCountry;
+
+ @JsonCreator
+ public CreditCardPaymentMethodInfo(@JsonProperty("id") String id,
+ @JsonProperty("accountId") String accountId,
+ @JsonProperty("defaultMethod") Boolean defaultMethod,
+ @JsonProperty("cardHolderName") String cardHolderName,
+ @JsonProperty("cardType") String cardType,
+ @JsonProperty("expirationDate") String expirationDate,
+ @JsonProperty("maskNumber") String maskNumber,
+ @JsonProperty("cardAddress1") String cardAddress1,
+ @JsonProperty("cardAddress2") String cardAddress2,
+ @JsonProperty("cardCity") String cardCity,
+ @JsonProperty("cardState") String cardState,
+ @JsonProperty("cardPostalCode") String cardPostalCode,
+ @JsonProperty("cardCountry") String cardCountry) {
+
+ super(id, accountId, defaultMethod, "CreditCard");
+ this.cardHolderName = cardHolderName;
+ this.cardType = cardType;
+ this.expirationDate = expirationDate;
+ this.maskNumber = maskNumber;
+ this.cardAddress1 = cardAddress1;
+ this.cardAddress2 = cardAddress2;
+ this.cardCity = cardCity;
+ this.cardState = cardState;
+ this.cardPostalCode = cardPostalCode;
+ this.cardCountry = cardCountry;
+ }
+
+ public String getCardHolderName() {
+ return cardHolderName;
+ }
+
+ public String getCardType() {
+ return cardType;
+ }
+
+ public String getCardAddress1() {
+ return cardAddress1;
+ }
+
+ public String getCardAddress2() {
+ return cardAddress2;
+ }
+
+ public String getCardCity() {
+ return cardCity;
+ }
+
+ public String getCardState() {
+ return cardState;
+ }
+
+ public String getCardPostalCode() {
+ return cardPostalCode;
+ }
+
+ public String getCardCountry() {
+ return cardCountry;
+ }
+
+ public String getExpirationDate() {
+ return expirationDate;
+ }
+
+ public String getMaskNumber() {
+ return maskNumber;
+ }
+
public static final class Builder extends BuilderBase<CreditCardPaymentMethodInfo, Builder> {
private String cardHolderName;
private String cardType;
@@ -115,84 +197,6 @@ public final class CreditCardPaymentMethodInfo extends PaymentMethodInfo {
}
}
- private final String cardHolderName;
- private final String cardType;
- private final String expirationDate;
- private final String maskNumber;
- private final String cardAddress1;
- private final String cardAddress2;
- private final String cardCity;
- private final String cardState;
- private final String cardPostalCode;
- private final String cardCountry;
-
- public CreditCardPaymentMethodInfo(String id,
- String accountId,
- Boolean defaultMethod,
- String cardHolderName,
- String cardType,
- String expirationDate,
- String maskNumber,
- String cardAddress1,
- String cardAddress2,
- String cardCity,
- String cardState,
- String cardPostalCode,
- String cardCountry) {
-
- super(id, accountId, defaultMethod, "CreditCard");
- this.cardHolderName = cardHolderName;
- this.cardType = cardType;
- this.expirationDate = expirationDate;
- this.maskNumber = maskNumber;
- this.cardAddress1 = cardAddress1;
- this.cardAddress2 = cardAddress2;
- this.cardCity = cardCity;
- this.cardState = cardState;
- this.cardPostalCode = cardPostalCode;
- this.cardCountry = cardCountry;
- }
-
- public String getCardHolderName() {
- return cardHolderName;
- }
-
- public String getCardType() {
- return cardType;
- }
-
- public String getCardAddress1() {
- return cardAddress1;
- }
-
- public String getCardAddress2() {
- return cardAddress2;
- }
-
- public String getCardCity() {
- return cardCity;
- }
-
- public String getCardState() {
- return cardState;
- }
-
- public String getCardPostalCode() {
- return cardPostalCode;
- }
-
- public String getCardCountry() {
- return cardCountry;
- }
-
- public String getExpirationDate() {
- return expirationDate;
- }
-
- public String getMaskNumber() {
- return maskNumber;
- }
-
@Override
public String toString() {
return "CreditCardPaymentMethodInfo [cardHolderName=" + cardHolderName + ", cardType=" + cardType + ", expirationDate=" + expirationDate + ", maskNumber=" + maskNumber + ", cardAddress1=" + cardAddress1 + ", cardAddress2=" + cardAddress2 + ", cardCity=" + cardCity + ", cardState=" + cardState + ", cardPostalCode=" + cardPostalCode + ", cardCountry=" + cardCountry + "]";
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
index fd84927..57c76a0 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentApi.java
@@ -39,7 +39,7 @@ public interface PaymentApi {
List<Either<PaymentError, PaymentInfo>> createPayment(String accountKey, List<String> invoiceIds);
List<Either<PaymentError, PaymentInfo>> createPayment(Account account, List<String> invoiceIds);
- Either<PaymentError, PaymentInfo> createPayment(UUID paymentAttemptId);
+ Either<PaymentError, PaymentInfo> createPaymentForPaymentAttempt(UUID paymentAttemptId);
List<Either<PaymentError, PaymentInfo>> createRefund(Account account, List<String> invoiceIds); //TODO
diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentError.java b/api/src/main/java/com/ning/billing/payment/api/PaymentError.java
index f1474c8..d9b8c49 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentError.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentError.java
@@ -15,6 +15,8 @@
*/
package com.ning.billing.payment.api;
+import java.util.UUID;
+
import org.codehaus.jackson.annotate.JsonTypeInfo;
import org.codehaus.jackson.annotate.JsonTypeInfo.Id;
@@ -24,15 +26,21 @@ import com.ning.billing.util.bus.BusEvent;
public class PaymentError implements BusEvent {
private final String type;
private final String message;
+ private final UUID accountId;
+ private final UUID invoiceId;
- public PaymentError(PaymentError src) {
+ public PaymentError(PaymentError src, UUID accountId, UUID invoiceId) {
this.type = src.type;
this.message = src.message;
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
}
- public PaymentError(String type, String message) {
+ public PaymentError(String type, String message, UUID accountId, UUID invoiceId) {
this.type = type;
this.message = message;
+ this.accountId = accountId;
+ this.invoiceId = invoiceId;
}
public String getType() {
@@ -43,10 +51,20 @@ public class PaymentError implements BusEvent {
return message;
}
+ public UUID getInvoiceId() {
+ return invoiceId;
+ }
+
+ public UUID getAccountId() {
+ return accountId;
+ }
+
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
+ result = prime * result + ((accountId == null) ? 0 : accountId.hashCode());
+ result = prime * result + ((invoiceId == null) ? 0 : invoiceId.hashCode());
result = prime * result + ((message == null) ? 0 : message.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
@@ -61,6 +79,18 @@ public class PaymentError implements BusEvent {
if (getClass() != obj.getClass())
return false;
PaymentError other = (PaymentError) obj;
+ if (accountId == null) {
+ if (other.accountId != null)
+ return false;
+ }
+ else if (!accountId.equals(other.accountId))
+ return false;
+ if (invoiceId == null) {
+ if (other.invoiceId != null)
+ return false;
+ }
+ else if (!invoiceId.equals(other.invoiceId))
+ return false;
if (message == null) {
if (other.message != null)
return false;
@@ -78,6 +108,7 @@ public class PaymentError implements BusEvent {
@Override
public String toString() {
- return "PaymentError [type=" + type + ", message=" + message + "]";
+ return "PaymentError [type=" + type + ", message=" + message + ", accountId=" + accountId + ", invoiceId=" + invoiceId + "]";
}
+
}
catalog/src/test/resources/WeaponsHire.xml 91(+54 -37)
diff --git a/catalog/src/test/resources/WeaponsHire.xml b/catalog/src/test/resources/WeaponsHire.xml
index 01d7cb4..3d36b9d 100644
--- a/catalog/src/test/resources/WeaponsHire.xml
+++ b/catalog/src/test/resources/WeaponsHire.xml
@@ -51,13 +51,13 @@ Use Cases to do:
<products>
<product name="Pistol">
<category>BASE</category>
- <available>
- <addonProduct>Telescopic-Scope</addonProduct>
- <addonProduct>Laser-Scope</addonProduct>
- </available>
</product>
<product name="Shotgun">
<category>BASE</category>
+ <available>
+ <addonProduct>Telescopic-Scope</addonProduct>
+ <addonProduct>Laser-Scope</addonProduct>
+ </available>
</product>
<product name="Assault-Rifle">
<category>BASE</category>
@@ -91,33 +91,18 @@ Use Cases to do:
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
- <changePolicyCase>
- <toProduct>Pistol</toProduct>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
+ <changePolicyCase>
+ <toProduct>Assault-Rifle</toProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ <changePolicyCase>
+ <fromProduct>Pistol</fromProduct>
+ <toProduct>Shotgun</toProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
<changePolicyCase>
<toPriceList>rescue</toPriceList>
<policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromProduct>Pistol</fromProduct>
- <toProduct>Shotgun</toProduct>
- <policy>IMMEDIATE</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromProduct>Assault-Rifle</fromProduct>
- <toProduct>Shotgun</toProduct>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromBillingPeriod>MONTHLY</fromBillingPeriod>
- <toProduct>Assault-Rifle</toProduct>
- <toBillingPeriod>MONTHLY</toBillingPeriod>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <toProduct>Assault-Rifle</toProduct>
- <policy>IMMEDIATE</policy>
</changePolicyCase>
<changePolicyCase>
<fromBillingPeriod>MONTHLY</fromBillingPeriod>
@@ -135,9 +120,6 @@ Use Cases to do:
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
- <alignment>START_OF_SUBSCRIPTION</alignment>
- </changeAlignmentCase>
- <changeAlignmentCase>
<toPriceList>rescue</toPriceList>
<alignment>CHANGE_OF_PLAN</alignment>
</changeAlignmentCase>
@@ -146,20 +128,27 @@ Use Cases to do:
<toPriceList>rescue</toPriceList>
<alignment>CHANGE_OF_PRICELIST</alignment>
</changeAlignmentCase>
+ <changeAlignmentCase>
+ <alignment>START_OF_SUBSCRIPTION</alignment>
+ </changeAlignmentCase>
</changeAlignment>
<cancelPolicy>
<cancelPolicyCase>
- <policy>END_OF_TERM</policy>
- </cancelPolicyCase>
- <cancelPolicyCase>
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</cancelPolicyCase>
+ <cancelPolicyCase>
+ <policy>END_OF_TERM</policy>
+ </cancelPolicyCase>
</cancelPolicy>
<createAlignment>
- <createAlignmentCase>
- <alignment>START_OF_BUNDLE</alignment>
- </createAlignmentCase>
+ <createAlignmentCase>
+ <product>Laser-Scope</product>
+ <alignment>START_OF_SUBSCRIPTION</alignment>
+ </createAlignmentCase>
+ <createAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </createAlignmentCase>
</createAlignment>
<billingAlignment>
<billingAlignmentCase>
@@ -447,6 +436,20 @@ Use Cases to do:
</plan>
<plan name="laser-scope-monthly">
<product>Laser-Scope</product>
+ <initialPhases>
+ <phase type="DISCOUNT">
+ <duration>
+ <unit>MONTHS</unit>
+ <number>1</number>
+ </duration>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price><currency>USD</currency><value>999.95</value></price>
+ <price><currency>EUR</currency><value>499.95</value></price>
+ <price><currency>GBP</currency><value>999.95</value></price>
+ </recurringPrice>
+ </phase>
+ </initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
@@ -461,6 +464,20 @@ Use Cases to do:
</plan>
<plan name="telescopic-scope-monthly">
<product>Telescopic-Scope</product>
+ <initialPhases>
+ <phase type="DISCOUNT">
+ <duration>
+ <unit>MONTHS</unit>
+ <number>1</number>
+ </duration>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price><currency>USD</currency><value>399.95</value></price>
+ <price><currency>EUR</currency><value>299.95</value></price>
+ <price><currency>GBP</currency><value>399.95</value></price>
+ </recurringPrice>
+ </phase>
+ </initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
entitlement/pom.xml 50(+26 -24)
diff --git a/entitlement/pom.xml b/entitlement/pom.xml
index cf8d926..137fb52 100644
--- a/entitlement/pom.xml
+++ b/entitlement/pom.xml
@@ -8,7 +8,8 @@
OR CONDITIONS OF ANY KIND, either express or implied. See the ~ License for
the specific language governing permissions and limitations ~ under the License. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ning.billing</groupId>
@@ -29,10 +30,6 @@
<artifactId>jdbi-metrics</artifactId>
</dependency>
<dependency>
- <groupId>mysql</groupId>
- <artifactId>mysql-connector-java</artifactId>
- </dependency>
- <dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
@@ -56,10 +53,10 @@
<artifactId>killbill-catalog</artifactId>
<scope>test</scope>
</dependency>
+ <!-- Should be in test scope , but broken right now -->
<dependency>
<groupId>com.ning.billing</groupId>
<artifactId>killbill-account</artifactId>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>com.ning.billing</groupId>
@@ -72,15 +69,33 @@
<artifactId>killbill-util</artifactId>
</dependency>
<dependency>
- <groupId>com.ning.billing</groupId>
- <artifactId>killbill-account</artifactId>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <scope>test</scope>
</dependency>
<dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>management</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>management-dbfiles</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <!-- Same here, this is really debatable whether or not we should keep that here -->
+ <dependency>
<groupId>com.ning.billing</groupId>
<artifactId>killbill-account</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
+
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <scope>runtime</scope>
+ </dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
@@ -126,10 +141,10 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
- <groups>setup,fast</groups>
+ <groups>fast,slow</groups>
</configuration>
</plugin>
- <plugin>
+ <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
@@ -144,26 +159,13 @@
</build>
<profiles>
<profile>
- <id>test-sql</id>
- <build>
- <plugins>
- <plugin>
- <artifactId>maven-surefire-plugin</artifactId>
- <configuration>
- <groups>setup,sql</groups>
- </configuration>
- </plugin>
- </plugins>
- </build>
- </profile>
- <profile>
<id>test-stress</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
- <groups>setup,stress</groups>
+ <groups>stress</groups>
</configuration>
</plugin>
</plugins>
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
index b7574e1..82a83bf 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/alignment/PlanAligner.java
@@ -21,6 +21,7 @@ import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.*;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionTransition;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.clock.DefaultClock;
import org.joda.time.DateTime;
@@ -61,10 +62,11 @@ public class PlanAligner {
* @throws CatalogApiException
* @throws EntitlementUserApiException
*/
- public TimedPhase [] getCurrentAndNextTimedPhaseOnCreate(SubscriptionData subscription,
- Plan plan, PhaseType initialPhase, String priceList, DateTime requestedDate, DateTime effectiveDate)
+ public TimedPhase [] getCurrentAndNextTimedPhaseOnCreate(final SubscriptionData subscription,
+ final Plan plan, final PhaseType initialPhase, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
throws CatalogApiException, EntitlementUserApiException {
- List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription, plan, initialPhase, priceList, requestedDate, effectiveDate);
+ List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getStartDate(),
+ subscription.getBundleStartDate(), plan, initialPhase, priceList, requestedDate);
TimedPhase [] result = new TimedPhase[2];
result[0] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.CURRENT);
result[1] = getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
@@ -83,8 +85,8 @@ public class PlanAligner {
* @throws CatalogApiException
* @throws EntitlementUserApiException
*/
- public TimedPhase getCurrentTimedPhaseOnChange(SubscriptionData subscription,
- Plan plan, String priceList, DateTime requestedDate, DateTime effectiveDate)
+ public TimedPhase getCurrentTimedPhaseOnChange(final SubscriptionData subscription,
+ final Plan plan, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
throws CatalogApiException, EntitlementUserApiException {
return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.CURRENT);
}
@@ -100,34 +102,65 @@ public class PlanAligner {
* @throws CatalogApiException
* @throws EntitlementUserApiException
*/
- public TimedPhase getNextTimedPhaseOnChange(SubscriptionData subscription,
- Plan plan, String priceList, DateTime requestedDate, DateTime effectiveDate)
+ public TimedPhase getNextTimedPhaseOnChange(final SubscriptionData subscription,
+ final Plan plan, final String priceList, final DateTime requestedDate, final DateTime effectiveDate)
throws CatalogApiException, EntitlementUserApiException {
return getTimedPhaseOnChange(subscription, plan, priceList, requestedDate, effectiveDate, WhichPhase.NEXT);
}
+
/**
- * Returns next future phase for that Plan based on effectiveDate
- *
- * @param plan
- * @param initialPhase the initial phase that subscription started on that Plan
- * @param effectiveDate the date used to consider what is future
- * @param initialStartPhase the date for when we started on that Plan/initialPhase
- * @return
- * @throws EntitlementError
+ * Returns next Phase for that Subscription at a point in time
+ * <p>
+ * @param subscription the subscription for which we need to compute the next Phase event
+ * @param effectiveDate the date at which we look to compute that event. effective needs to be after last Plan change or initial Plan
+ * @return The PhaseEvent at the correct point in time
*/
- public TimedPhase getNextTimedPhase(Plan plan, PhaseType initialPhase, DateTime effectiveDate, DateTime initialStartPhase)
- throws EntitlementError {
+ public TimedPhase getNextTimedPhase(final SubscriptionData subscription, final DateTime requestedDate, final DateTime effectiveDate) {
try {
- List<TimedPhase> timedPhases = getPhaseAlignments(plan, initialPhase, initialStartPhase);
- return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
- } catch (EntitlementUserApiException e) {
- throw new EntitlementError(String.format("Could not compute next phase change for plan %s with initialPhase %s", plan.getName(), initialPhase));
+
+ SubscriptionTransition lastPlanTransition = subscription.getInitialTransitionForCurrentPlan();
+ if (effectiveDate.isBefore(lastPlanTransition.getEffectiveTransitionTime())) {
+ throw new EntitlementError(String.format("Cannot specify an effectiveDate prior to last Plan Change, subscription = %s, effectiveDate = %s",
+ subscription.getId(), effectiveDate));
+ }
+
+ switch(lastPlanTransition.getTransitionType()) {
+ // If we never had any Plan change, borrow the logics for createPlan alignment
+ case MIGRATE_ENTITLEMENT:
+ case CREATE:
+ List<TimedPhase> timedPhases = getTimedPhaseOnCreate(subscription.getStartDate(),
+ subscription.getBundleStartDate(),
+ lastPlanTransition.getNextPlan(),
+ lastPlanTransition.getNextPhase().getPhaseType(),
+ lastPlanTransition.getNextPriceList(),
+ requestedDate);
+ return getTimedPhase(timedPhases, effectiveDate, WhichPhase.NEXT);
+ // If we went through Plan changes, borrow the logics for changePlan alignement
+ case CHANGE:
+ return getTimedPhaseOnChange(subscription.getStartDate(),
+ subscription.getBundleStartDate(),
+ lastPlanTransition.getPreviousPhase(),
+ lastPlanTransition.getPreviousPlan(),
+ lastPlanTransition.getPreviousPriceList(),
+ lastPlanTransition.getNextPlan(),
+ lastPlanTransition.getNextPriceList(),
+ requestedDate,
+ effectiveDate,
+ WhichPhase.NEXT);
+ default:
+ throw new EntitlementError(String.format("Unexpectd initial transition %s for current plan %s on subscription %s",
+ lastPlanTransition.getTransitionType(), subscription.getCurrentPlan(), subscription.getId()));
+ }
+ } catch (Exception /* EntitlementUserApiException, CatalogApiException */ e) {
+ throw new EntitlementError(String.format("Could not compute next phase change for subscription %s", subscription.getId()), e);
}
}
- private List<TimedPhase> getTimedPhaseOnCreate(SubscriptionData subscription,
- Plan plan, PhaseType initialPhase, String priceList, DateTime requestedDate, DateTime effectiveDate)
+
+ private List<TimedPhase> getTimedPhaseOnCreate(DateTime subscriptionStartDate,
+ DateTime bundleStartDate,
+ Plan plan, PhaseType initialPhase, String priceList, DateTime requestedDate)
throws CatalogApiException, EntitlementUserApiException {
Catalog catalog = catalogService.getFullCatalog();
@@ -138,15 +171,13 @@ public class PlanAligner {
priceList);
DateTime planStartDate = null;
- PlanAlignmentCreate alignement = null;
- alignement = catalog.planCreateAlignment(planSpecifier, requestedDate);
-
+ PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, requestedDate);
switch(alignement) {
case START_OF_SUBSCRIPTION:
- planStartDate = subscription.getStartDate();
+ planStartDate = subscriptionStartDate;
break;
case START_OF_BUNDLE:
- planStartDate = subscription.getBundleStartDate();
+ planStartDate = bundleStartDate;
break;
default:
throw new EntitlementError(String.format("Unknwon PlanAlignmentCreate %s", alignement));
@@ -155,36 +186,55 @@ public class PlanAligner {
}
private TimedPhase getTimedPhaseOnChange(SubscriptionData subscription,
- Plan plan, String priceList, DateTime requestedDate, DateTime effectiveDate, WhichPhase which)
+ Plan nextPlan, String nextPriceList, DateTime requestedDate, DateTime effectiveDate, WhichPhase which)
throws CatalogApiException, EntitlementUserApiException {
+ return getTimedPhaseOnChange(subscription.getStartDate(),
+ subscription.getBundleStartDate(),
+ subscription.getCurrentPhase(),
+ subscription.getCurrentPlan(),
+ subscription.getCurrentPriceList(),
+ nextPlan,
+ nextPriceList,
+ requestedDate,
+ effectiveDate,
+ which);
+ }
- Catalog catalog = catalogService.getFullCatalog();
- PlanPhase currentPhase = subscription.getCurrentPhase();
- Plan currentPlan = subscription.getCurrentPlan();
- String currentPriceList = subscription.getCurrentPriceList();
+ private TimedPhase getTimedPhaseOnChange(DateTime subscriptionStartDate,
+ DateTime bundleStartDate,
+ PlanPhase currentPhase,
+ Plan currentPlan,
+ String currentPriceList,
+ Plan nextPlan, String priceList, DateTime requestedDate, DateTime effectiveDate, WhichPhase which)
+ throws CatalogApiException, EntitlementUserApiException {
+
+ Catalog catalog = catalogService.getFullCatalog();
+ ProductCategory currentCategory = currentPlan.getProduct().getCategory();
+ // STEPH tiered ADDON not implemented yet
+ if (currentCategory != ProductCategory.BASE) {
+ throw new EntitlementError(String.format("Only implemented changePlan for BasePlan"));
+ }
PlanPhaseSpecifier fromPlanPhaseSpecifier = new PlanPhaseSpecifier(currentPlan.getProduct().getName(),
- currentPlan.getProduct().getCategory(),
+ currentCategory,
currentPlan.getBillingPeriod(),
currentPriceList,
currentPhase.getPhaseType());
- PlanSpecifier toPlanSpecifier = new PlanSpecifier(plan.getProduct().getName(),
- plan.getProduct().getCategory(),
- plan.getBillingPeriod(),
+ PlanSpecifier toPlanSpecifier = new PlanSpecifier(nextPlan.getProduct().getName(),
+ nextPlan.getProduct().getCategory(),
+ nextPlan.getBillingPeriod(),
priceList);
DateTime planStartDate = null;
-
- PlanAlignmentChange alignment = null;
- alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, requestedDate);
+ PlanAlignmentChange alignment = catalog.planChangeAlignment(fromPlanPhaseSpecifier, toPlanSpecifier, requestedDate);
switch(alignment) {
case START_OF_SUBSCRIPTION:
- planStartDate = subscription.getStartDate();
+ planStartDate = subscriptionStartDate;
break;
case START_OF_BUNDLE:
- planStartDate = subscription.getBundleStartDate();
+ planStartDate = bundleStartDate;
break;
case CHANGE_OF_PLAN:
planStartDate = requestedDate;
@@ -194,7 +244,7 @@ public class PlanAligner {
default:
throw new EntitlementError(String.format("Unknwon PlanAlignmentChange %s", alignment));
}
- List<TimedPhase> timedPhases = getPhaseAlignments(plan, null, planStartDate);
+ List<TimedPhase> timedPhases = getPhaseAlignments(nextPlan, null, planStartDate);
return getTimedPhase(timedPhases, effectiveDate, which);
}
@@ -209,6 +259,7 @@ public class PlanAligner {
DateTime curPhaseStart = (initialPhase == null) ? initialPhaseStartDate : null;
DateTime nextPhaseStart = null;
for (PlanPhase cur : plan.getAllPhases()) {
+ // For create we can specify the phase so skip any phase until we reach initialPhase
if (curPhaseStart == null) {
if (initialPhase != cur.getPhaseType()) {
continue;
@@ -252,7 +303,7 @@ public class PlanAligner {
case NEXT:
return next;
default:
- throw new EntitlementError(String.format("Unepected %s TimedPhase", which));
+ throw new EntitlementError(String.format("Unexpected %s TimedPhase", which));
}
}
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
index 7a1f9a9..0b372e8 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/migration/DefaultEntitlementMigrationApi.java
@@ -18,20 +18,15 @@ package com.ning.billing.entitlement.api.migration;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import org.joda.time.DateTime;
+import com.google.common.collect.Lists;
import com.google.inject.Inject;
-import com.ning.billing.catalog.api.CatalogApiException;
-import com.ning.billing.catalog.api.CatalogService;
-import com.ning.billing.catalog.api.Duration;
-import com.ning.billing.catalog.api.PhaseType;
-import com.ning.billing.catalog.api.Plan;
-import com.ning.billing.catalog.api.PlanPhase;
-import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.alignment.MigrationPlanAligner;
import com.ning.billing.entitlement.alignment.TimedMigration;
@@ -52,7 +47,6 @@ import com.ning.billing.entitlement.events.user.ApiEventChange;
import com.ning.billing.entitlement.events.user.ApiEventMigrate;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.clock.Clock;
-import com.ning.billing.util.clock.DefaultClock;
public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
@@ -60,19 +54,16 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
private final EntitlementDao dao;
private final MigrationPlanAligner migrationAligner;
private final SubscriptionFactory factory;
- private final CatalogService catalogService;
private final Clock clock;
@Inject
public DefaultEntitlementMigrationApi(MigrationPlanAligner migrationAligner,
SubscriptionFactory factory,
- CatalogService catalogService,
EntitlementDao dao,
Clock clock) {
this.dao = dao;
this.migrationAligner = migrationAligner;
this.factory = factory;
- this.catalogService = catalogService;
this.clock = clock;
}
@@ -101,20 +92,39 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
SubscriptionBundleData bundleData = new SubscriptionBundleData(curBundle.getBundleKey(), accountId);
List<SubscriptionMigrationData> bundleSubscriptionData = new LinkedList<AccountMigrationData.SubscriptionMigrationData>();
- for (EntitlementSubscriptionMigration curSub : curBundle.getSubscriptions()) {
+
+ List<EntitlementSubscriptionMigration> sortedSubscriptions = Lists.newArrayList(curBundle.getSubscriptions());
+ // Make sure we have first mpp or legacy, then addon and for each category order by CED
+ Collections.sort(sortedSubscriptions, new Comparator<EntitlementSubscriptionMigration>() {
+ @Override
+ public int compare(EntitlementSubscriptionMigration o1,
+ EntitlementSubscriptionMigration o2) {
+ if (o1.getCategory().equals(o2.getCategory())) {
+ return o1.getSubscriptionCases()[0].getEffectiveDate().compareTo(o2.getSubscriptionCases()[0].getEffectiveDate());
+ } else {
+ if (o1.getCategory().equals("mpp")) {
+ return -1;
+ } else if (o2.getCategory().equals("mpp")) {
+ return 1;
+ } else if (o1.getCategory().equals("legacy")) {
+ return -1;
+ } else if (o2.getCategory().equals("legacy")) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ });
+
+ DateTime bundleStartDate = null;
+ for (EntitlementSubscriptionMigration curSub : sortedSubscriptions) {
SubscriptionMigrationData data = null;
- switch (curSub.getCategory()) {
- case BASE:
- data = createBaseSubscriptionMigrationData(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now);
- break;
- case ADD_ON:
- // Not implemented yet
- break;
- case STANDALONE:
- data = createStandaloneSubscriptionMigrationData(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now);
- break;
- default:
- throw new EntitlementMigrationApiException(String.format("Unkown product type ", curSub.getCategory()));
+ if (bundleStartDate == null) {
+ data = createInitialSubscription(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now);
+ bundleStartDate = data.getInitialEvents().get(0).getEffectiveDate();
+ } else {
+ data = createSubscriptionMigrationDataWithBundleDate(bundleData.getId(), curSub.getCategory(), curSub.getSubscriptionCases(), now, bundleStartDate);
}
if (data != null) {
bundleSubscriptionData.add(data);
@@ -127,7 +137,7 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
return accountMigrationData;
}
- private SubscriptionMigrationData createBaseSubscriptionMigrationData(UUID bundleId, ProductCategory productCategory,
+ private SubscriptionMigrationData createInitialSubscription(UUID bundleId, ProductCategory productCategory,
EntitlementSubscriptionMigrationCase [] input, DateTime now)
throws EntitlementMigrationApiException {
@@ -144,8 +154,8 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
return new SubscriptionMigrationData(subscriptionData, toEvents(subscriptionData, now, events));
}
- private SubscriptionMigrationData createStandaloneSubscriptionMigrationData(UUID bundleId, ProductCategory productCategory,
- EntitlementSubscriptionMigrationCase [] input, DateTime now)
+ private SubscriptionMigrationData createSubscriptionMigrationDataWithBundleDate(UUID bundleId, ProductCategory productCategory,
+ EntitlementSubscriptionMigrationCase [] input, DateTime now, DateTime bundleStartDate)
throws EntitlementMigrationApiException {
TimedMigration [] events = migrationAligner.getEventsMigration(input, now);
DateTime migrationStartDate= events[0].getEventTime();
@@ -154,7 +164,7 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
.setId(UUID.randomUUID())
.setBundleId(bundleId)
.setCategory(productCategory)
- .setBundleStartDate(migrationStartDate)
+ .setBundleStartDate(bundleStartDate)
.setStartDate(migrationStartDate),
emptyEvents);
return new SubscriptionMigrationData(subscriptionData, toEvents(subscriptionData, now, events));
@@ -179,7 +189,8 @@ public class DefaultEntitlementMigrationApi implements EntitlementMigrationApi {
.setActiveVersion(subscriptionData.getActiveVersion())
.setEffectiveDate(cur.getEventTime())
.setProcessedDate(now)
- .setRequestedDate(now);
+ .setRequestedDate(now)
+ .setFromDisk(true);
switch(cur.getApiEventType()) {
case MIGRATE_ENTITLEMENT:
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
index ed05b02..5bd8596 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/DefaultEntitlementUserApi.java
@@ -27,7 +27,10 @@ import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.Product;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.clock.Clock;
@@ -39,14 +42,17 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
private final EntitlementDao dao;
private final CatalogService catalogService;
private final SubscriptionApiService apiService;
+ private final AddonUtils addonUtils;
@Inject
- public DefaultEntitlementUserApi(Clock clock, EntitlementDao dao, CatalogService catalogService, SubscriptionApiService apiService) {
+ public DefaultEntitlementUserApi(Clock clock, EntitlementDao dao, CatalogService catalogService,
+ SubscriptionApiService apiService, AddonUtils addonUtils) {
super();
this.clock = clock;
this.apiService = apiService;
this.dao = dao;
this.catalogService = catalogService;
+ this.addonUtils = addonUtils;
}
@Override
@@ -87,6 +93,7 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
return dao.createSubscriptionBundle(bundle);
}
+
@Override
public Subscription createSubscription(UUID bundleId, PlanPhaseSpecifier spec, DateTime requestedDate) throws EntitlementUserApiException {
try {
@@ -96,12 +103,11 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
if (requestedDate != null && requestedDate.isAfter(now)) {
throw new EntitlementUserApiException(ErrorCode.ENT_INVALID_REQUESTED_DATE, requestedDate.toString());
}
- requestedDate = (requestedDate == null) ? now : requestedDate;
DateTime effectiveDate = requestedDate;
Plan plan = catalogService.getFullCatalog().findPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, requestedDate);
- PlanPhase phase = (plan.getInitialPhases() != null) ? plan.getInitialPhases()[0] : plan.getFinalPhase();
+ PlanPhase phase = plan.getAllPhases()[0];
if (phase == null) {
throw new EntitlementError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
@@ -113,8 +119,7 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
}
DateTime bundleStartDate = null;
- Subscription baseSubscription = dao.getBaseSubscription(bundleId);
-
+ SubscriptionData baseSubscription = (SubscriptionData) dao.getBaseSubscription(bundleId);
switch(plan.getProduct().getCategory()) {
case BASE:
if (baseSubscription != null) {
@@ -126,19 +131,27 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
if (baseSubscription == null) {
throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_NO_BP, bundleId);
}
+ checkAddonCreationRights(baseSubscription, plan);
bundleStartDate = baseSubscription.getStartDate();
break;
+ case STANDALONE:
+ if (baseSubscription != null) {
+ throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_BP_EXISTS, bundleId);
+ }
+ // Not really but we don't care, there is no alignment for STANDALONE subscriptions
+ bundleStartDate = requestedDate;
+ break;
default:
throw new EntitlementError(String.format("Can't create subscription of type %s",
plan.getProduct().getCategory().toString()));
}
- SubscriptionData subscription = apiService.createBasePlan(new SubscriptionBuilder()
- .setId(UUID.randomUUID())
- .setBundleId(bundleId)
- .setCategory(plan.getProduct().getCategory())
- .setBundleStartDate(bundleStartDate)
- .setStartDate(effectiveDate),
+ SubscriptionData subscription = apiService.createPlan(new SubscriptionBuilder()
+ .setId(UUID.randomUUID())
+ .setBundleId(bundleId)
+ .setCategory(plan.getProduct().getCategory())
+ .setBundleStartDate(bundleStartDate)
+ .setStartDate(effectiveDate),
plan, spec.getPhaseType(), realPriceList, requestedDate, effectiveDate, now);
return subscription;
@@ -147,6 +160,26 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
}
}
+
+ private void checkAddonCreationRights(SubscriptionData baseSubscription, Plan targetAddOnPlan)
+ throws EntitlementUserApiException, CatalogApiException {
+
+ if (baseSubscription.getState() != SubscriptionState.ACTIVE) {
+ throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_BP_NON_ACTIVE, targetAddOnPlan.getName());
+ }
+
+ Product baseProduct = baseSubscription.getCurrentPlan().getProduct();
+ if (addonUtils.isAddonIncluded(baseProduct, targetAddOnPlan)) {
+ throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_ALREADY_INCLUDED,
+ targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
+ }
+
+ if (!addonUtils.isAddonAvailable(baseProduct, targetAddOnPlan)) {
+ throw new EntitlementUserApiException(ErrorCode.ENT_CREATE_AO_NOT_AVAILABLE,
+ targetAddOnPlan.getName(), baseSubscription.getCurrentPlan().getProduct().getName());
+ }
+ }
+
@Override
public DateTime getNextBillingDate(UUID accountId) {
List<SubscriptionBundle> bundles = getBundlesForAccount(accountId);
@@ -155,7 +188,7 @@ public class DefaultEntitlementUserApi implements EntitlementUserApi {
List<Subscription> subscriptions = getSubscriptionsForBundle(bundle.getId());
for(Subscription subscription : subscriptions) {
DateTime chargedThruDate = subscription.getChargedThroughDate();
- if(result == null ||
+ if(result == null ||
(chargedThruDate != null && chargedThruDate.isBefore(result))) {
result = subscription.getChargedThroughDate();
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
index d825e12..9a82a99 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionApiService.java
@@ -52,13 +52,13 @@ public class SubscriptionApiService {
- public SubscriptionData createBasePlan(SubscriptionBuilder builder, Plan plan, PhaseType initialPhase,
+ public SubscriptionData createPlan(SubscriptionBuilder builder, Plan plan, PhaseType initialPhase,
String realPriceList, DateTime requestedDate, DateTime effectiveDate, DateTime processedDate)
throws EntitlementUserApiException {
try {
- SubscriptionData subscription = new SubscriptionData(builder, this, clock);
+ SubscriptionData subscription = new SubscriptionData(builder, this, clock);
TimedPhase [] curAndNextPhases = planAligner.getCurrentAndNextTimedPhaseOnCreate(subscription, plan, initialPhase, realPriceList, requestedDate, effectiveDate);
ApiEventCreate creationEvent = new ApiEventCreate(new ApiEventBuilder()
@@ -69,7 +69,8 @@ public class SubscriptionApiService {
.setActiveVersion(subscription.getActiveVersion())
.setProcessedDate(processedDate)
.setEffectiveDate(effectiveDate)
- .setRequestedDate(requestedDate));
+ .setRequestedDate(requestedDate)
+ .setFromDisk(true));
TimedPhase nextTimedPhase = curAndNextPhases[1];
PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
@@ -117,7 +118,8 @@ public class SubscriptionApiService {
.setActiveVersion(subscription.getActiveVersion())
.setProcessedDate(now)
.setEffectiveDate(effectiveDate)
- .setRequestedDate(now));
+ .setRequestedDate(now)
+ .setFromDisk(true));
dao.cancelSubscription(subscription.getId(), cancelEvent);
subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId()), catalogService.getFullCatalog());
@@ -140,13 +142,13 @@ public class SubscriptionApiService {
.setActiveVersion(subscription.getActiveVersion())
.setProcessedDate(now)
.setRequestedDate(now)
- .setEffectiveDate(now));
+ .setEffectiveDate(now)
+ .setFromDisk(true));
List<EntitlementEvent> uncancelEvents = new ArrayList<EntitlementEvent>();
uncancelEvents.add(uncancelEvent);
- DateTime planStartDate = subscription.getCurrentPlanStart();
- TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription.getCurrentPlan(), subscription.getInitialPhaseOnCurrentPlan().getPhaseType(), now, planStartDate);
+ TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
null;
@@ -164,7 +166,6 @@ public class SubscriptionApiService {
try {
-
DateTime now = clock.getUTCNow();
requestedDate = (requestedDate != null) ? DefaultClock.truncateMs(requestedDate) : now;
validateRequestedDateOnChangeOrCancel(subscription, now, requestedDate);
@@ -214,7 +215,8 @@ public class SubscriptionApiService {
.setActiveVersion(subscription.getActiveVersion())
.setProcessedDate(now)
.setEffectiveDate(effectiveDate)
- .setRequestedDate(now));
+ .setRequestedDate(now)
+ .setFromDisk(true));
TimedPhase nextTimedPhase = planAligner.getNextTimedPhaseOnChange(subscription, newPlan, newPriceList.getName(), requestedDate, effectiveDate);
PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionBundleData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionBundleData.java
index 8bdd584..8cc2573 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionBundleData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionBundleData.java
@@ -54,6 +54,7 @@ public class SubscriptionBundleData implements SubscriptionBundle {
return accountId;
}
+
// STEPH do we need it ? and should we return that and when is that populated/updated?
@Override
public DateTime getStartDate() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
index 48f24df..ce341fd 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionData.java
@@ -18,6 +18,7 @@ package com.ning.billing.entitlement.api.user;
import com.ning.billing.ErrorCode;
import com.ning.billing.catalog.api.*;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
@@ -208,9 +209,11 @@ public class SubscriptionData implements Subscription {
}
// ensure that the latestSubscription is always set; prevents NPEs
- SubscriptionTransition latestSubscription = transitions.get(0);
- for (SubscriptionTransition cur : transitions) {
- if (cur.getEffectiveTransitionTime().isAfter(clock.getUTCNow())) {
+ SubscriptionTransitionData latestSubscription = transitions.get(0);
+ for (SubscriptionTransitionData cur : transitions) {
+ if (cur.getEffectiveTransitionTime().isAfter(clock.getUTCNow()) ||
+ // We are not looking at events that were patched on the fly-- such as future ADDON cancelation from Base Plan
+ !cur.isFromDisk()) {
break;
}
latestSubscription = cur;
@@ -235,6 +238,7 @@ public class SubscriptionData implements Subscription {
return activeVersion;
}
+ @Override
public ProductCategory getCategory() {
return category;
}
@@ -253,15 +257,7 @@ public class SubscriptionData implements Subscription {
return paidThroughDate;
}
- public DateTime getCurrentPlanStart() {
- return getInitialTransitionForCurrentPlan().getEffectiveTransitionTime();
- }
-
- public PlanPhase getInitialPhaseOnCurrentPlan() {
- return getInitialTransitionForCurrentPlan().getNextPhase();
- }
-
- private SubscriptionTransitionData getInitialTransitionForCurrentPlan() {
+ public SubscriptionTransitionData getInitialTransitionForCurrentPlan() {
if (transitions == null) {
throw new EntitlementError(String.format("No transitions for subscription %s", getId()));
}
@@ -366,6 +362,8 @@ public class SubscriptionData implements Subscription {
ApiEventType apiEventType = null;
+ boolean isFromDisk = true;
+
switch (cur.getType()) {
case PHASE:
@@ -376,6 +374,7 @@ public class SubscriptionData implements Subscription {
case API_USER:
ApiEvent userEV = (ApiEvent) cur;
apiEventType = userEV.getEventType();
+ isFromDisk = userEV.isFromDisk();
switch(apiEventType) {
case MIGRATE_ENTITLEMENT:
case CREATE:
@@ -437,7 +436,8 @@ public class SubscriptionData implements Subscription {
nextState,
nextPlan,
nextPhase,
- nextPriceList);
+ nextPriceList,
+ isFromDisk);
transitions.add(transition);
previousState = nextState;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
index 5a0eba0..fb8e2f2 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/api/user/SubscriptionTransitionData.java
@@ -44,11 +44,12 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
private final String nextPriceList;
private final Plan nextPlan;
private final PlanPhase nextPhase;
+ private final boolean isFromDisk;
public SubscriptionTransitionData(UUID eventId, UUID subscriptionId, UUID bundleId, EventType eventType,
ApiEventType apiEventType, DateTime requestedTransitionTime, DateTime effectiveTransitionTime,
SubscriptionState previousState, Plan previousPlan, PlanPhase previousPhase, String previousPriceList,
- SubscriptionState nextState, Plan nextPlan, PlanPhase nextPhase, String nextPriceList) {
+ SubscriptionState nextState, Plan nextPlan, PlanPhase nextPhase, String nextPriceList, boolean isFromDisk) {
super();
this.eventId = eventId;
this.subscriptionId = subscriptionId;
@@ -65,6 +66,7 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
this.nextPlan = nextPlan;
this.nextPriceList = nextPriceList;
this.nextPhase = nextPhase;
+ this.isFromDisk = isFromDisk;
}
@Override
@@ -136,14 +138,6 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
}
}
- public ApiEventType getApiEventType() {
- return apiEventType;
- }
-
- public EventType getEventType() {
- return eventType;
- }
-
@Override
public DateTime getRequestedTransitionTime() {
return requestedTransitionTime;
@@ -154,6 +148,19 @@ public class SubscriptionTransitionData implements SubscriptionTransition {
return effectiveTransitionTime;
}
+ public boolean isFromDisk() {
+ return isFromDisk;
+ }
+
+ public ApiEventType getApiEventType() {
+ return apiEventType;
+ }
+
+ public EventType getEventType() {
+ return eventType;
+ }
+
+
@Override
public String toString() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java
new file mode 100644
index 0000000..b2c9405
--- /dev/null
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/addon/AddonUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.entitlement.engine.addon;
+
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.Inject;
+import com.ning.billing.ErrorCode;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.CatalogService;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
+import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.api.user.SubscriptionTransition;
+import com.ning.billing.entitlement.exceptions.EntitlementError;
+
+public class AddonUtils {
+
+ private static final Logger logger = LoggerFactory.getLogger(AddonUtils.class);
+
+ private final CatalogService catalogService;
+
+ @Inject
+ public AddonUtils(CatalogService catalogService) {
+ this.catalogService = catalogService;
+ }
+
+
+ public boolean isAddonAvailable(final String basePlanName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+ Product product = plan.getProduct();
+ return isAddonAvailable(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new EntitlementError(e);
+ }
+ }
+
+ public boolean isAddonAvailable(final Product baseProduct, final Plan targetAddOnPlan) {
+ Product targetAddonProduct = targetAddOnPlan.getProduct();
+ Product[] availableAddOns = baseProduct.getAvailable();
+
+ for (Product curAv : availableAddOns) {
+ if (curAv.getName().equals(targetAddonProduct.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isAddonIncluded(final String basePlanName, final DateTime requestedDate, final Plan targetAddOnPlan) {
+ try {
+ Plan plan = catalogService.getFullCatalog().findPlan(basePlanName, requestedDate);
+ Product product = plan.getProduct();
+ return isAddonIncluded(product, targetAddOnPlan);
+ } catch (CatalogApiException e) {
+ throw new EntitlementError(e);
+ }
+ }
+
+ public boolean isAddonIncluded(final Product baseProduct, final Plan targetAddOnPlan) {
+ Product targetAddonProduct = targetAddOnPlan.getProduct();
+ Product[] includedAddOns = baseProduct.getIncluded();
+ for (Product curAv : includedAddOns) {
+ if (curAv.getName().equals(targetAddonProduct.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
index 6832ed9..55ea2cf 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/core/Engine.java
@@ -16,13 +16,22 @@
package com.ning.billing.entitlement.engine.core;
+
+
+import java.util.Iterator;
+import java.util.List;
import java.util.UUID;
+
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
+
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.config.EntitlementConfig;
import com.ning.billing.entitlement.alignment.PlanAligner;
import com.ning.billing.entitlement.alignment.TimedPhase;
@@ -33,12 +42,18 @@ import com.ning.billing.entitlement.api.migration.DefaultEntitlementMigrationApi
import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
import com.ning.billing.entitlement.api.user.DefaultEntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
+import com.ning.billing.entitlement.api.user.Subscription;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
import com.ning.billing.entitlement.api.user.SubscriptionData;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
import com.ning.billing.entitlement.events.phase.PhaseEvent;
import com.ning.billing.entitlement.events.phase.PhaseEventData;
+import com.ning.billing.entitlement.events.user.ApiEvent;
+import com.ning.billing.entitlement.events.user.ApiEventBuilder;
+import com.ning.billing.entitlement.events.user.ApiEventCancel;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.lifecycle.LifecycleHandlerType;
import com.ning.billing.lifecycle.LifecycleHandlerType.LifecycleLevel;
@@ -64,7 +79,9 @@ public class Engine implements EventListener, EntitlementService {
private final EntitlementUserApi userApi;
private final EntitlementBillingApi billingApi;
private final EntitlementMigrationApi migrationApi;
+ private final AddonUtils addonUtils;
private final Bus eventBus;
+
private final EntitlementConfig config;
private final NotificationQueueService notificationQueueService;
@@ -74,7 +91,7 @@ public class Engine implements EventListener, EntitlementService {
public Engine(Clock clock, EntitlementDao dao, PlanAligner planAligner,
EntitlementConfig config, DefaultEntitlementUserApi userApi,
DefaultEntitlementBillingApi billingApi,
- DefaultEntitlementMigrationApi migrationApi, Bus eventBus,
+ DefaultEntitlementMigrationApi migrationApi, AddonUtils addonUtils, Bus eventBus,
NotificationQueueService notificationQueueService) {
super();
this.clock = clock;
@@ -83,6 +100,7 @@ public class Engine implements EventListener, EntitlementService {
this.userApi = userApi;
this.billingApi = billingApi;
this.migrationApi = migrationApi;
+ this.addonUtils = addonUtils;
this.config = config;
this.eventBus = eventBus;
this.notificationQueueService = notificationQueueService;
@@ -172,8 +190,14 @@ public class Engine implements EventListener, EntitlementService {
log.warn("Failed to retrieve subscription for id %s", event.getSubscriptionId());
return;
}
+ //
+ // Do any internal processing on that event before we send the event to the bus
+ //
if (event.getType() == EventType.PHASE) {
- insertNextPhaseEvent(subscription);
+ onPhaseEvent(subscription);
+ } else if (event.getType() == EventType.API_USER &&
+ subscription.getCategory() == ProductCategory.BASE) {
+ onBasePlanEvent(subscription, (ApiEvent) event);
}
try {
eventBus.post(subscription.getTransitionFromEvent(event));
@@ -182,10 +206,11 @@ public class Engine implements EventListener, EntitlementService {
}
}
- private void insertNextPhaseEvent(SubscriptionData subscription) {
+
+ private void onPhaseEvent(SubscriptionData subscription) {
try {
DateTime now = clock.getUTCNow();
- TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription.getCurrentPlan(), subscription.getInitialPhaseOnCurrentPlan().getPhaseType(), now, subscription.getCurrentPlanStart());
+ TimedPhase nextTimedPhase = planAligner.getNextTimedPhase(subscription, now, now);
PhaseEvent nextPhaseEvent = (nextTimedPhase != null) ?
PhaseEventData.createNextPhaseEvent(nextTimedPhase.getPhase().getName(), subscription, now, nextTimedPhase.getStartPhase()) :
null;
@@ -197,4 +222,38 @@ public class Engine implements EventListener, EntitlementService {
}
}
+ private void onBasePlanEvent(SubscriptionData baseSubscription, ApiEvent event) {
+
+ DateTime now = clock.getUTCNow();
+
+ Product baseProduct = (baseSubscription.getState() == SubscriptionState.CANCELLED ) ?
+ null : baseSubscription.getCurrentPlan().getProduct();
+
+ List<Subscription> subscriptions = dao.getSubscriptions(baseSubscription.getBundleId());
+
+ Iterator<Subscription> it = subscriptions.iterator();
+ while (it.hasNext()) {
+ SubscriptionData cur = (SubscriptionData) it.next();
+ if (cur.getState() == SubscriptionState.CANCELLED ||
+ cur.getCategory() != ProductCategory.ADD_ON) {
+ continue;
+ }
+ Plan addonCurrentPlan = cur.getCurrentPlan();
+ if (baseProduct == null ||
+ addonUtils.isAddonIncluded(baseProduct, addonCurrentPlan) ||
+ ! addonUtils.isAddonAvailable(baseProduct, addonCurrentPlan)) {
+ //
+ // Perform AO cancellation using the effectiveDate of the BP
+ //
+ EntitlementEvent cancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(cur.getId())
+ .setActiveVersion(cur.getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(event.getEffectiveDate())
+ .setRequestedDate(now)
+ .setFromDisk(true));
+ dao.cancelSubscription(cur.getId(), cancelEvent);
+ }
+ }
+ }
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
index a1a3dc5..ef3168f 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EntitlementSqlDao.java
@@ -17,7 +17,9 @@
package com.ning.billing.entitlement.engine.dao;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@@ -30,8 +32,12 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
import com.google.inject.Inject;
import com.ning.billing.ErrorCode;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.Product;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.migration.AccountMigrationData;
import com.ning.billing.entitlement.api.migration.AccountMigrationData.BundleMigrationData;
@@ -42,10 +48,14 @@ import com.ning.billing.entitlement.api.user.SubscriptionBundleData;
import com.ning.billing.entitlement.api.user.SubscriptionData;
import com.ning.billing.entitlement.api.user.SubscriptionFactory;
import com.ning.billing.entitlement.api.user.SubscriptionFactory.SubscriptionBuilder;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
import com.ning.billing.entitlement.engine.core.Engine;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.EntitlementEvent.EventType;
import com.ning.billing.entitlement.events.user.ApiEvent;
+import com.ning.billing.entitlement.events.user.ApiEventBuilder;
+import com.ning.billing.entitlement.events.user.ApiEventCancel;
+import com.ning.billing.entitlement.events.user.ApiEventChange;
import com.ning.billing.entitlement.events.user.ApiEventType;
import com.ning.billing.entitlement.exceptions.EntitlementError;
import com.ning.billing.util.clock.Clock;
@@ -65,16 +75,18 @@ public class EntitlementSqlDao implements EntitlementDao {
private final EventSqlDao eventsDao;
private final SubscriptionFactory factory;
private final NotificationQueueService notificationQueueService;
+ private final AddonUtils addonUtils;
@Inject
public EntitlementSqlDao(final IDBI dbi, final Clock clock, final SubscriptionFactory factory,
- final NotificationQueueService notificationQueueService) {
+ final AddonUtils addonUtils, final NotificationQueueService notificationQueueService) {
this.clock = clock;
this.factory = factory;
this.subscriptionsDao = dbi.onDemand(SubscriptionSqlDao.class);
this.eventsDao = dbi.onDemand(EventSqlDao.class);
this.bundlesDao = dbi.onDemand(BundleSqlDao.class);
this.notificationQueueService = notificationQueueService;
+ this.addonUtils = addonUtils;
}
@Override
@@ -104,10 +116,6 @@ public class EntitlementSqlDao implements EntitlementDao {
});
}
- @Override
- public Subscription getSubscriptionFromId(final UUID subscriptionId) {
- return buildSubscription(subscriptionsDao.getSubscriptionFromId(subscriptionId.toString()));
- }
@Override
public UUID getAccountIdFromSubscriptionId(final UUID subscriptionId) {
@@ -134,19 +142,18 @@ public class EntitlementSqlDao implements EntitlementDao {
@Override
public Subscription getBaseSubscription(final UUID bundleId) {
+ return getBaseSubscription(bundleId, true);
+ }
- List<Subscription> subscriptions = subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString());
- for (Subscription cur : subscriptions) {
- if (((SubscriptionData)cur).getCategory() == ProductCategory.BASE) {
- return buildSubscription(cur);
- }
- }
- return null;
+
+ @Override
+ public Subscription getSubscriptionFromId(final UUID subscriptionId) {
+ return buildSubscription(subscriptionsDao.getSubscriptionFromId(subscriptionId.toString()));
}
@Override
public List<Subscription> getSubscriptions(UUID bundleId) {
- return buildSubscription(subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString()));
+ return buildBundleSubscriptions(subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString()));
}
@Override
@@ -155,7 +162,7 @@ public class EntitlementSqlDao implements EntitlementDao {
if (bundle == null) {
return Collections.emptyList();
}
- return buildSubscription(subscriptionsDao.getSubscriptionsFromBundleId(bundle.getId().toString()));
+ return getSubscriptions(bundle.getId());
}
@Override
@@ -243,6 +250,7 @@ public class EntitlementSqlDao implements EntitlementDao {
@Override
public Void inTransaction(EventSqlDao dao,
TransactionStatus status) throws Exception {
+ cancelNextCancelEventFromTransaction(subscriptionId, dao);
cancelNextChangeEventFromTransaction(subscriptionId, dao);
cancelNextPhaseEventFromTransaction(subscriptionId, dao);
dao.insertEvent(cancelEvent);
@@ -332,6 +340,10 @@ public class EntitlementSqlDao implements EntitlementDao {
cancelFutureEventFromTransaction(subscriptionId, dao, EventType.API_USER, ApiEventType.CHANGE);
}
+ private void cancelNextCancelEventFromTransaction(final UUID subscriptionId, final EventSqlDao dao) {
+ cancelFutureEventFromTransaction(subscriptionId, dao, EventType.API_USER, ApiEventType.CANCEL);
+ }
+
private void cancelFutureEventFromTransaction(final UUID subscriptionId, final EventSqlDao dao, EventType type, ApiEventType apiType) {
UUID futureEventId = null;
@@ -354,18 +366,106 @@ public class EntitlementSqlDao implements EntitlementDao {
}
}
+ public Subscription getBaseSubscription(final UUID bundleId, boolean rebuildSubscription) {
+ List<Subscription> subscriptions = subscriptionsDao.getSubscriptionsFromBundleId(bundleId.toString());
+ for (Subscription cur : subscriptions) {
+ if (((SubscriptionData)cur).getCategory() == ProductCategory.BASE) {
+ return rebuildSubscription ? buildSubscription(cur) : cur;
+ }
+ }
+ return null;
+ }
+
+
private Subscription buildSubscription(Subscription input) {
if (input == null) {
return null;
}
- return buildSubscription(Collections.singletonList(input)).get(0);
+ List<Subscription> bundleInput = new ArrayList<Subscription>();
+ Subscription baseSubscription = null;
+ if (input.getCategory() == ProductCategory.ADD_ON) {
+ baseSubscription = getBaseSubscription(input.getBundleId(), false);
+ bundleInput.add(baseSubscription);
+ bundleInput.add(input);
+ } else {
+ bundleInput.add(input);
+ }
+ List<Subscription> reloadedSubscriptions = buildBundleSubscriptions(bundleInput);
+ for (Subscription cur : reloadedSubscriptions) {
+ if (cur.getId().equals(input.getId())) {
+ return cur;
+ }
+ }
+ throw new EntitlementError(String.format("Unexpected code path in buildSubscription"));
}
- private List<Subscription> buildSubscription(List<Subscription> input) {
- List<Subscription> result = new ArrayList<Subscription>(input.size());
+
+
+ private List<Subscription> buildBundleSubscriptions(List<Subscription> input) {
+
+ // Make sure BasePlan -- if exists-- is first
+ Collections.sort(input, new Comparator<Subscription>() {
+ @Override
+ public int compare(Subscription o1, Subscription o2) {
+ if (o1.getCategory() == ProductCategory.BASE) {
+ return -1;
+ } else if (o2.getCategory() == ProductCategory.BASE) {
+ return 1;
+ } else {
+ return o1.getStartDate().compareTo(o2.getStartDate());
+ }
+ }
+ });
+
+ EntitlementEvent futureBaseEvent = null;
+ List<Subscription> result = new ArrayList<Subscription>(input.size());
for (Subscription cur : input) {
+
List<EntitlementEvent> events = eventsDao.getEventsForSubscription(cur.getId().toString());
- Subscription reloaded = factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+ Subscription reloaded = factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+
+ switch (cur.getCategory()) {
+ case BASE:
+ Collection<EntitlementEvent> futureApiEvents = Collections2.filter(events, new Predicate<EntitlementEvent>() {
+ @Override
+ public boolean apply(EntitlementEvent input) {
+ return (input.getEffectiveDate().isAfter(clock.getUTCNow()) &&
+ ((input instanceof ApiEventCancel) || (input instanceof ApiEventChange)));
+ }
+ });
+ futureBaseEvent = (futureApiEvents.size() == 0) ? null : futureApiEvents.iterator().next();
+ break;
+
+ case ADD_ON:
+ Plan targetAddOnPlan = reloaded.getCurrentPlan();
+ String baseProductName = (futureBaseEvent instanceof ApiEventChange) ?
+ ((ApiEventChange) futureBaseEvent).getEventPlan() : null;
+
+ boolean createCancelEvent = (futureBaseEvent != null) &&
+ ((futureBaseEvent instanceof ApiEventCancel) ||
+ ((! addonUtils.isAddonAvailable(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan)) ||
+ (addonUtils.isAddonIncluded(baseProductName, futureBaseEvent.getEffectiveDate(), targetAddOnPlan))));
+
+ if (createCancelEvent) {
+ DateTime now = clock.getUTCNow();
+ EntitlementEvent addOnCancelEvent = new ApiEventCancel(new ApiEventBuilder()
+ .setSubscriptionId(reloaded.getId())
+ .setActiveVersion(((SubscriptionData) reloaded).getActiveVersion())
+ .setProcessedDate(now)
+ .setEffectiveDate(futureBaseEvent.getEffectiveDate())
+ .setRequestedDate(now)
+ // This event is only there to indicate the ADD_ON is future canceled, but it is not there
+ // on disk until the base plan cancellation becomes effective
+ .setFromDisk(false));
+
+ events.add(addOnCancelEvent);
+ // Finally reload subscription with full set of events
+ reloaded = factory.createSubscription(new SubscriptionBuilder((SubscriptionData) cur), events);
+ }
+ break;
+ default:
+ break;
+ }
result.add(reloaded);
}
return result;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
index 5f485e5..4e2128e 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/engine/dao/EventSqlDao.java
@@ -139,7 +139,8 @@ public interface EventSqlDao extends Transactional<EventSqlDao>, CloseMe, Transm
.setEventPlan(planName)
.setEventPlanPhase(phaseName)
.setEventPriceList(priceListName)
- .setEventType(userType);
+ .setEventType(userType)
+ .setFromDisk(true);
if (userType == ApiEventType.CREATE) {
result = new ApiEventCreate(builder);
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
index ecd5aa1..c26b168 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEvent.java
@@ -29,4 +29,6 @@ public interface ApiEvent extends EntitlementEvent {
public String getPriceList();
+ public boolean isFromDisk();
+
}
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
index 2438ddf..f67c6b7 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBase.java
@@ -28,7 +28,7 @@ public class ApiEventBase extends EventBase implements ApiEvent {
private final String eventPlan;
private final String eventPlanPhase;
private final String eventPriceList;
-
+ private final boolean fromDisk;
public ApiEventBase(ApiEventBuilder builder) {
super(builder);
@@ -36,9 +36,10 @@ public class ApiEventBase extends EventBase implements ApiEvent {
this.eventPriceList = builder.getEventPriceList();
this.eventPlan = builder.getEventPlan();
this.eventPlanPhase = builder.getEventPlanPhase();
+ this.fromDisk = builder.isFromDisk();
}
-
+/*
public ApiEventBase(UUID subscriptionId, DateTime bundleStartDate, DateTime processed, String planName, String phaseName,
String priceList, DateTime requestedDate, ApiEventType eventType, DateTime effectiveDate, long activeVersion) {
super(subscriptionId, requestedDate, effectiveDate, processed, activeVersion, true);
@@ -56,7 +57,7 @@ public class ApiEventBase extends EventBase implements ApiEvent {
this.eventPlan = null;
this.eventPlanPhase = null;
}
-
+*/
@Override
public ApiEventType getEventType() {
@@ -83,6 +84,11 @@ public class ApiEventBase extends EventBase implements ApiEvent {
return eventPriceList;
}
+ @Override
+ public boolean isFromDisk() {
+ return fromDisk;
+ }
+
@Override
public String toString() {
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
index 2ff026b..b7e9764 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/events/user/ApiEventBuilder.java
@@ -24,6 +24,8 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
private String eventPlan;
private String eventPlanPhase;
private String eventPriceList;
+ private boolean fromDisk;
+
public ApiEventBuilder() {
super();
@@ -49,6 +51,15 @@ public class ApiEventBuilder extends EventBaseBuilder<ApiEventBuilder> {
return eventPriceList;
}
+ public boolean isFromDisk() {
+ return fromDisk;
+ }
+
+ public ApiEventBuilder setFromDisk(boolean fromDisk) {
+ this.fromDisk = fromDisk;
+ return this;
+ }
+
public ApiEventBuilder setEventType(ApiEventType eventType) {
this.eventType = eventType;
return this;
diff --git a/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java b/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
index 4f0dfec..704b765 100644
--- a/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
+++ b/entitlement/src/main/java/com/ning/billing/entitlement/glue/EntitlementModule.java
@@ -30,6 +30,7 @@ import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
import com.ning.billing.entitlement.api.user.DefaultEntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.SubscriptionApiService;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
import com.ning.billing.entitlement.engine.core.Engine;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.engine.dao.EntitlementSqlDao;
@@ -54,6 +55,7 @@ public class EntitlementModule extends AbstractModule {
bind(EntitlementService.class).to(Engine.class).asEagerSingleton();
bind(Engine.class).asEagerSingleton();
bind(PlanAligner.class).asEagerSingleton();
+ bind(AddonUtils.class).asEagerSingleton();
bind(MigrationPlanAligner.class).asEagerSingleton();
bind(EntitlementUserApi.class).to(DefaultEntitlementUserApi.class).asEagerSingleton();
bind(EntitlementBillingApi.class).to(DefaultEntitlementBillingApi.class).asEagerSingleton();
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
index 98cc376..fab66dd 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/BrainDeadSubscription.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -131,14 +132,13 @@ public class BrainDeadSubscription implements Subscription {
@Override
public List<SubscriptionTransition> getAllTransitions() {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException();
}
@Override
public SubscriptionTransition getPendingTransition() {
throw new UnsupportedOperationException();
-
}
@Override
@@ -146,4 +146,9 @@ public class BrainDeadSubscription implements Subscription {
return null;
}
+ @Override
+ public ProductCategory getCategory() {
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
index 2d99f8d..55d383a 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/billing/TestDefaultEntitlementBillingApi.java
@@ -29,6 +29,7 @@ import org.joda.time.DateTimeZone;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;
import com.google.inject.Guice;
@@ -66,7 +67,7 @@ public class TestDefaultEntitlementBillingApi {
private static final UUID zeroId = new UUID(0L,0L);
private static final UUID oneId = new UUID(1L,0L);
private static final UUID twoId = new UUID(2L,0L);
-
+
private CatalogService catalogService;
private ArrayList<SubscriptionBundle> bundles;
private ArrayList<Subscription> subscriptions;
@@ -77,51 +78,55 @@ public class TestDefaultEntitlementBillingApi {
private SubscriptionData subscription;
private DateTime subscriptionStartDate;
- @BeforeClass(groups={"setup"})
+ @BeforeSuite(alwaysRun=true)
public void setup() throws ServiceException {
TestApiBase.loadSystemPropertiesFromClasspath("/entitlement.properties");
final Injector g = Guice.createInjector(Stage.PRODUCTION, new CatalogModule(), new ClockModule());
-
+
catalogService = g.getInstance(CatalogService.class);
clock = g.getInstance(Clock.class);
-
+
((DefaultCatalogService)catalogService).loadCatalog();
}
-
+
@BeforeMethod(alwaysRun=true)
public void setupEveryTime() {
bundles = new ArrayList<SubscriptionBundle>();
final SubscriptionBundle bundle = new SubscriptionBundleData( zeroId,"TestKey", oneId, clock.getUTCNow().minusDays(4));
bundles.add(bundle);
-
-
+
+
transitions = new ArrayList<SubscriptionTransition>();
subscriptions = new ArrayList<Subscription>();
-
+
SubscriptionBuilder builder = new SubscriptionBuilder();
subscriptionStartDate = clock.getUTCNow().minusDays(3);
builder.setStartDate(subscriptionStartDate).setId(oneId);
subscription = new SubscriptionData(builder) {
- public List<SubscriptionTransition> getAllTransitions() {
+ @Override
+ public List<SubscriptionTransition> getAllTransitions() {
return transitions;
}
};
subscriptions.add(subscription);
-
+
dao = new BrainDeadMockEntitlementDao() {
- public List<SubscriptionBundle> getSubscriptionBundleForAccount(
+ @Override
+ public List<SubscriptionBundle> getSubscriptionBundleForAccount(
UUID accountId) {
return bundles;
-
+
}
- public List<Subscription> getSubscriptions(UUID bundleId) {
+ @Override
+ public List<Subscription> getSubscriptions(UUID bundleId) {
return subscriptions;
}
- public Subscription getSubscriptionFromId(UUID subscriptionId) {
+ @Override
+ public Subscription getSubscriptionFromId(UUID subscriptionId) {
return subscription;
}
@@ -139,11 +144,12 @@ public class TestDefaultEntitlementBillingApi {
assertTrue(true);
}
-
+
@Test(enabled=true, groups="fast")
public void testBillingEventsEmpty() {
EntitlementDao dao = new BrainDeadMockEntitlementDao() {
- public List<SubscriptionBundle> getSubscriptionBundleForAccount(
+ @Override
+ public List<SubscriptionBundle> getSubscriptionBundleForAccount(
UUID accountId) {
return new ArrayList<SubscriptionBundle>();
}
@@ -159,7 +165,7 @@ public class TestDefaultEntitlementBillingApi {
SortedSet<BillingEvent> events = api.getBillingEventsForAccount(new UUID(0L,0L));
Assert.assertEquals(events.size(), 0);
}
-
+
@Test(enabled=true, groups="fast")
public void testBillingEventsNoBillingPeriod() throws CatalogApiException {
DateTime now = clock.getUTCNow();
@@ -168,9 +174,9 @@ public class TestDefaultEntitlementBillingApi {
PlanPhase nextPhase = nextPlan.getAllPhases()[0]; // The trial has no billing period
String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
SubscriptionTransition t = new SubscriptionTransitionData(
- zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+ zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
transitions.add(t);
-
+
AccountUserApi accountApi = new BrainDeadAccountUserApi(){
@Override
@@ -193,20 +199,20 @@ public class TestDefaultEntitlementBillingApi {
PlanPhase nextPhase = nextPlan.getAllPhases()[1];
String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
SubscriptionTransition t = new SubscriptionTransitionData(
- zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+ zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
transitions.add(t);
-
- Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
+
+ Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
((ZombieControl)account).addResult("getBillCycleDay", 1).addResult("getTimeZone", DateTimeZone.UTC);
-
- AccountUserApi accountApi = BrainDeadProxyFactory.createBrainDeadProxyFor(AccountUserApi.class);
+
+ AccountUserApi accountApi = BrainDeadProxyFactory.createBrainDeadProxyFor(AccountUserApi.class);
((ZombieControl)accountApi).addResult("getAccountById", account);
-
+
DefaultEntitlementBillingApi api = new DefaultEntitlementBillingApi(dao,accountApi,catalogService);
SortedSet<BillingEvent> events = api.getBillingEventsForAccount(new UUID(0L,0L));
checkFirstEvent(events, nextPlan, subscription.getStartDate().getDayOfMonth(), oneId, now, nextPhase, ApiEventType.CREATE.toString());
}
-
+
@Test(enabled=true, groups="fast")
public void testBillingEventsMonthly() throws CatalogApiException {
DateTime now = clock.getUTCNow();
@@ -215,9 +221,9 @@ public class TestDefaultEntitlementBillingApi {
PlanPhase nextPhase = nextPlan.getAllPhases()[1];
String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
SubscriptionTransition t = new SubscriptionTransitionData(
- zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+ zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
transitions.add(t);
-
+
AccountUserApi accountApi = new BrainDeadAccountUserApi(){
@Override
@@ -231,7 +237,7 @@ public class TestDefaultEntitlementBillingApi {
SortedSet<BillingEvent> events = api.getBillingEventsForAccount(new UUID(0L,0L));
checkFirstEvent(events, nextPlan, 32, oneId, now, nextPhase, ApiEventType.CREATE.toString());
}
-
+
@Test(enabled=true, groups="fast")
public void testBillingEventsAddOn() throws CatalogApiException {
DateTime now = clock.getUTCNow();
@@ -240,15 +246,15 @@ public class TestDefaultEntitlementBillingApi {
PlanPhase nextPhase = nextPlan.getAllPhases()[0];
String nextPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
SubscriptionTransition t = new SubscriptionTransitionData(
- zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList);
+ zeroId, oneId, twoId, EventType.API_USER, ApiEventType.CREATE, then, now, null, null, null, null, SubscriptionState.ACTIVE, nextPlan, nextPhase, nextPriceList, true);
transitions.add(t);
-
- Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
+
+ Account account = BrainDeadProxyFactory.createBrainDeadProxyFor(Account.class);
((ZombieControl)account).addResult("getBillCycleDay", 1).addResult("getTimeZone", DateTimeZone.UTC);
-
- AccountUserApi accountApi = BrainDeadProxyFactory.createBrainDeadProxyFor(AccountUserApi.class);
+
+ AccountUserApi accountApi = BrainDeadProxyFactory.createBrainDeadProxyFor(AccountUserApi.class);
((ZombieControl)accountApi).addResult("getAccountById", account);
-
+
DefaultEntitlementBillingApi api = new DefaultEntitlementBillingApi(dao,accountApi,catalogService);
SortedSet<BillingEvent> events = api.getBillingEventsForAccount(new UUID(0L,0L));
checkFirstEvent(events, nextPlan, bundles.get(0).getStartDate().getDayOfMonth(), oneId, now, nextPhase, ApiEventType.CREATE.toString());
@@ -265,7 +271,7 @@ public class TestDefaultEntitlementBillingApi {
if(nextPhase.getRecurringPrice() != null) {
Assert.assertEquals(nextPhase.getRecurringPrice().getPrice(Currency.USD), event.getRecurringPrice().getPrice(Currency.USD));
}
-
+
Assert.assertEquals(BCD, event.getBillCycleDay());
Assert.assertEquals(id, event.getSubscription().getId());
Assert.assertEquals(time, event.getEffectiveDate());
@@ -277,6 +283,6 @@ public class TestDefaultEntitlementBillingApi {
Assert.assertEquals(nextPhase.getFixedPrice(), event.getFixedPrice());
Assert.assertEquals(nextPhase.getRecurringPrice(), event.getRecurringPrice());
}
-
-
+
+
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
index 7ef459d..acd64ea 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigration.java
@@ -29,6 +29,7 @@ import java.util.UUID;
import org.joda.time.DateTime;
import org.testng.Assert;
+import com.google.common.collect.Lists;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Duration;
import com.ning.billing.catalog.api.PhaseType;
@@ -78,6 +79,48 @@ public abstract class TestMigration extends TestApiBase {
}
+ public void testPlanWithAddOn() {
+ try {
+ DateTime beforeMigration = clock.getUTCNow();
+ final DateTime initalAddonStart = clock.getUTCNow().minusMonths(1).plusDays(7);
+ EntitlementAccountMigration toBeMigrated = createAccountWithRegularBasePlanAndAddons(initalAddonStart);
+ DateTime afterMigration = clock.getUTCNow();
+
+ testListener.pushExpectedEvent(NextEvent.MIGRATE_ENTITLEMENT);
+ migrationApi.migrate(toBeMigrated);
+ assertTrue(testListener.isCompleted(5000));
+
+ List<SubscriptionBundle> bundles = entitlementApi.getBundlesForAccount(toBeMigrated.getAccountKey());
+ assertEquals(bundles.size(), 1);
+ SubscriptionBundle bundle = bundles.get(0);
+
+ List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundle.getId());
+ assertEquals(subscriptions.size(), 2);
+
+ Subscription baseSubscription = (subscriptions.get(0).getCurrentPlan().getProduct().getCategory() == ProductCategory.BASE) ?
+ subscriptions.get(0) : subscriptions.get(1);
+ assertDateWithin(baseSubscription.getStartDate(), beforeMigration, afterMigration);
+ assertEquals(baseSubscription.getEndDate(), null);
+ assertEquals(baseSubscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(baseSubscription.getCurrentPhase().getPhaseType(), PhaseType.EVERGREEN);
+ assertEquals(baseSubscription.getState(), SubscriptionState.ACTIVE);
+ assertEquals(baseSubscription.getCurrentPlan().getName(), "assault-rifle-annual");
+
+ Subscription aoSubscription = (subscriptions.get(0).getCurrentPlan().getProduct().getCategory() == ProductCategory.ADD_ON) ?
+ subscriptions.get(0) : subscriptions.get(1);
+ assertEquals(aoSubscription.getStartDate(), initalAddonStart);
+ assertEquals(aoSubscription.getEndDate(), null);
+ assertEquals(aoSubscription.getCurrentPriceList(), PriceListSet.DEFAULT_PRICELIST_NAME);
+ assertEquals(aoSubscription.getCurrentPhase().getPhaseType(), PhaseType.DISCOUNT);
+ assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+ assertEquals(aoSubscription.getCurrentPlan().getName(), "telescopic-scope-monthly");
+
+ } catch (EntitlementMigrationApiException e) {
+ Assert.fail("", e);
+ }
+ }
+
+
public void testSingleBasePlanFutureCancelled() {
try {
@@ -212,7 +255,7 @@ public abstract class TestMigration extends TestApiBase {
}
- private EntitlementAccountMigration createAccountWithSingleBasePlan(final List<EntitlementSubscriptionMigrationCase> cases) {
+ private EntitlementAccountMigration createAccountWithSingleBasePlan(final List<List<EntitlementSubscriptionMigrationCase>> cases) {
return new EntitlementAccountMigration() {
@@ -225,18 +268,24 @@ public abstract class TestMigration extends TestApiBase {
@Override
public EntitlementSubscriptionMigration[] getSubscriptions() {
- EntitlementSubscriptionMigration subscription = new EntitlementSubscriptionMigration() {
- @Override
- public EntitlementSubscriptionMigrationCase[] getSubscriptionCases() {
- return cases.toArray(new EntitlementSubscriptionMigrationCase[cases.size()]);
- }
- @Override
- public ProductCategory getCategory() {
- return ProductCategory.BASE;
- }
- };
- EntitlementSubscriptionMigration[] result = new EntitlementSubscriptionMigration[1];
- result[0] = subscription;
+
+ EntitlementSubscriptionMigration[] result = new EntitlementSubscriptionMigration[cases.size()];
+
+ for (int i = 0; i < cases.size(); i++) {
+
+ final List<EntitlementSubscriptionMigrationCase> curCases = cases.get(i);
+ EntitlementSubscriptionMigration subscription = new EntitlementSubscriptionMigration() {
+ @Override
+ public EntitlementSubscriptionMigrationCase[] getSubscriptionCases() {
+ return curCases.toArray(new EntitlementSubscriptionMigrationCase[curCases.size()]);
+ }
+ @Override
+ public ProductCategory getCategory() {
+ return ProductCategory.BASE;
+ }
+ };
+ result[i] = subscription;
+ }
return result;
}
@Override
@@ -255,6 +304,61 @@ public abstract class TestMigration extends TestApiBase {
};
}
+ private EntitlementAccountMigration createAccountWithRegularBasePlanAndAddons(final DateTime initalAddonStart) {
+
+ List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+ cases.add(new EntitlementSubscriptionMigrationCase() {
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+ return new PlanPhaseSpecifier("Assault-Rifle", ProductCategory.BASE, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return clock.getUTCNow().minusMonths(3);
+ }
+ @Override
+ public DateTime getCancelledDate() {
+ return null;
+ }
+ });
+
+ List<EntitlementSubscriptionMigrationCase> firstAddOnCases = new LinkedList<EntitlementSubscriptionMigrationCase>();
+
+ firstAddOnCases.add(new EntitlementSubscriptionMigrationCase() {
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+ return new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.DISCOUNT);
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return initalAddonStart;
+ }
+ @Override
+ public DateTime getCancelledDate() {
+ return initalAddonStart.plusMonths(1);
+ }
+ });
+ firstAddOnCases.add(new EntitlementSubscriptionMigrationCase() {
+ @Override
+ public PlanPhaseSpecifier getPlanPhaseSpecifer() {
+ return new PlanPhaseSpecifier("Telescopic-Scope", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, PhaseType.EVERGREEN);
+ }
+ @Override
+ public DateTime getEffectiveDate() {
+ return initalAddonStart.plusMonths(1);
+ }
+ @Override
+ public DateTime getCancelledDate() {
+ return null;
+ }
+ });
+
+ List<List<EntitlementSubscriptionMigrationCase>> input = new ArrayList<List<EntitlementSubscriptionMigrationCase>>();
+ input.add(cases);
+ input.add(firstAddOnCases);
+ return createAccountWithSingleBasePlan(input);
+ }
+
private EntitlementAccountMigration createAccountWithRegularBasePlan() {
List<EntitlementSubscriptionMigrationCase> cases = new LinkedList<EntitlementSubscriptionMigrationCase>();
cases.add(new EntitlementSubscriptionMigrationCase() {
@@ -271,7 +375,9 @@ public abstract class TestMigration extends TestApiBase {
return null;
}
});
- return createAccountWithSingleBasePlan(cases);
+ List<List<EntitlementSubscriptionMigrationCase>> input = new ArrayList<List<EntitlementSubscriptionMigrationCase>>();
+ input.add(cases);
+ return createAccountWithSingleBasePlan(input);
}
private EntitlementAccountMigration createAccountWithRegularBasePlanFutreCancelled() {
@@ -291,7 +397,9 @@ public abstract class TestMigration extends TestApiBase {
return effectiveDate.plusYears(1);
}
});
- return createAccountWithSingleBasePlan(cases);
+ List<List<EntitlementSubscriptionMigrationCase>> input = new ArrayList<List<EntitlementSubscriptionMigrationCase>>();
+ input.add(cases);
+ return createAccountWithSingleBasePlan(input);
}
@@ -325,7 +433,9 @@ public abstract class TestMigration extends TestApiBase {
return null;
}
});
- return createAccountWithSingleBasePlan(cases);
+ List<List<EntitlementSubscriptionMigrationCase>> input = new ArrayList<List<EntitlementSubscriptionMigrationCase>>();
+ input.add(cases);
+ return createAccountWithSingleBasePlan(input);
}
private EntitlementAccountMigration createAccountFuturePendingChange() {
@@ -359,7 +469,9 @@ public abstract class TestMigration extends TestApiBase {
return null;
}
});
- return createAccountWithSingleBasePlan(cases);
+ List<List<EntitlementSubscriptionMigrationCase>> input = new ArrayList<List<EntitlementSubscriptionMigrationCase>>();
+ input.add(cases);
+ return createAccountWithSingleBasePlan(input);
}
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
index b3eb168..84744d8 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/migration/TestMigrationSql.java
@@ -31,25 +31,31 @@ public class TestMigrationSql extends TestMigration {
}
@Override
- @Test(enabled=true, groups="sql")
+ @Test(enabled=true, groups="slow")
public void testSingleBasePlan() {
super.testSingleBasePlan();
}
@Override
- @Test(enabled=true, groups="sql")
+ @Test(enabled=true, groups="slow")
+ public void testPlanWithAddOn() {
+ super.testPlanWithAddOn();
+ }
+
+ @Override
+ @Test(enabled=true, groups="slow")
public void testSingleBasePlanFutureCancelled() {
super.testSingleBasePlanFutureCancelled();
}
@Override
- @Test(enabled=true, groups="sql")
+ @Test(enabled=true, groups="slow")
public void testSingleBasePlanWithPendingPhase() {
super.testSingleBasePlanWithPendingPhase();
}
@Override
- @Test(enabled=true, groups="sql")
+ @Test(enabled=true, groups="slow")
public void testSingleBasePlanWithPendingChange() {
super.testSingleBasePlanWithPendingChange();
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
index 912f186..094faba 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/TestApiBase.java
@@ -26,6 +26,7 @@ import java.net.URL;
import java.util.List;
import java.util.UUID;
+import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
@@ -33,8 +34,10 @@ import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
+import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.BeforeSuite;
import com.google.inject.Injector;
import com.ning.billing.account.api.AccountData;
@@ -49,6 +52,7 @@ import com.ning.billing.catalog.api.PlanPhaseSpecifier;
import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.catalog.api.TimeUnit;
import com.ning.billing.config.EntitlementConfig;
+import com.ning.billing.dbi.MysqlTestingHelper;
import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
import com.ning.billing.entitlement.api.migration.EntitlementMigrationApi;
@@ -60,11 +64,11 @@ import com.ning.billing.entitlement.api.user.SubscriptionTransition;
import com.ning.billing.entitlement.engine.core.Engine;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.engine.dao.MockEntitlementDao;
+import com.ning.billing.entitlement.engine.dao.MockEntitlementDaoMemory;
import com.ning.billing.entitlement.events.EntitlementEvent;
import com.ning.billing.entitlement.events.phase.PhaseEvent;
import com.ning.billing.entitlement.events.user.ApiEvent;
import com.ning.billing.entitlement.events.user.ApiEventType;
-import com.ning.billing.lifecycle.KillbillService.ServiceException;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.clock.ClockMock;
import com.ning.billing.util.bus.DefaultBusService;
@@ -94,8 +98,9 @@ public abstract class TestApiBase {
protected ApiTestListener testListener;
protected SubscriptionBundle bundle;
- public static void loadSystemPropertiesFromClasspath(final String resource)
- {
+ private MysqlTestingHelper helper;
+
+ public static void loadSystemPropertiesFromClasspath(final String resource) {
final URL url = TestApiBase.class.getResource(resource);
assertNotNull(url);
@@ -106,7 +111,9 @@ public abstract class TestApiBase {
}
}
- @AfterClass(groups={"setup"})
+ protected abstract Injector getInjector();
+
+ @AfterClass(alwaysRun=true)
public void tearDown() {
try {
busService.getBus().register(testListener);
@@ -117,7 +124,7 @@ public abstract class TestApiBase {
}
- @BeforeClass(groups={"setup"})
+ @BeforeClass(alwaysRun=true)
public void setup() {
loadSystemPropertiesFromClasspath("/entitlement.properties");
@@ -129,21 +136,35 @@ public abstract class TestApiBase {
config = g.getInstance(EntitlementConfig.class);
dao = g.getInstance(EntitlementDao.class);
clock = (ClockMock) g.getInstance(Clock.class);
+ helper = (isSqlTest(dao)) ? g.getInstance(MysqlTestingHelper.class) : null;
+
try {
((DefaultCatalogService) catalogService).loadCatalog();
((DefaultBusService) busService).startBus();
((Engine) entitlementService).initialize();
init();
- } catch (EntitlementUserApiException e) {
- Assert.fail(e.getMessage());
- } catch (ServiceException e) {
- Assert.fail(e.getMessage());
+ } catch (Exception e) {
}
}
- protected abstract Injector getInjector();
+ private static boolean isSqlTest(EntitlementDao theDao) {
+ return (! (theDao instanceof MockEntitlementDaoMemory));
+ }
+
+ private void setupMySQL() throws IOException {
+ if (helper != null) {
+ final String entitlementDdl = IOUtils.toString(TestApiBase.class.getResourceAsStream("/com/ning/billing/entitlement/ddl.sql"));
+ final String utilDdl = IOUtils.toString(TestApiBase.class.getResourceAsStream("/com/ning/billing/util/ddl.sql"));
+ helper.startMysql();
+ helper.initDb(entitlementDdl);
+ helper.initDb(utilDdl);
+ }
+ }
+
+ private void init() throws Exception {
+
+ setupMySQL();
- private void init() throws EntitlementUserApiException {
accountData = getAccountData();
assertNotNull(accountData);
@@ -155,10 +176,9 @@ public abstract class TestApiBase {
entitlementApi = entitlementService.getUserApi();
billingApi = entitlementService.getBillingApi();
migrationApi = entitlementService.getMigrationApi();
-
}
- @BeforeMethod(groups={"setup"})
+ @BeforeMethod(alwaysRun=true)
public void setupTest() {
log.warn("\n");
@@ -180,17 +200,20 @@ public abstract class TestApiBase {
((Engine)entitlementService).start();
}
- @AfterMethod(groups={"setup"})
+ @AfterMethod(alwaysRun=true)
public void cleanupTest() {
-
-
((Engine)entitlementService).stop();
log.warn("DONE WITH TEST\n");
}
protected SubscriptionData createSubscription(final String productName, final BillingPeriod term, final String planSet) throws EntitlementUserApiException {
+ return createSubscriptionWithBundle(bundle.getId(), productName, term, planSet);
+ }
+
+
+ protected SubscriptionData createSubscriptionWithBundle(final UUID bundleId, final String productName, final BillingPeriod term, final String planSet) throws EntitlementUserApiException {
testListener.pushExpectedEvent(NextEvent.CREATE);
- SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundle.getId(),
+ SubscriptionData subscription = (SubscriptionData) entitlementApi.createSubscription(bundleId,
new PlanPhaseSpecifier(productName, ProductCategory.BASE, term, planSet, null),
clock.getUTCNow());
assertNotNull(subscription);
@@ -342,12 +365,12 @@ public abstract class TestApiBase {
@Override
public String getAddress1() {
- return null;
+ return null;
}
@Override
public String getAddress2() {
- return null;
+ return null;
}
@Override
@@ -357,22 +380,22 @@ public abstract class TestApiBase {
@Override
public String getCity() {
- return null;
+ return null;
}
@Override
public String getStateOrProvince() {
- return null;
+ return null;
}
@Override
public String getPostalCode() {
- return null;
+ return null;
}
@Override
public String getCountry() {
- return null;
+ return null;
}
};
return accountData;
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
new file mode 100644
index 0000000..7db938e
--- /dev/null
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiAddOn.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.entitlement.api.user;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import com.ning.billing.catalog.api.BillingPeriod;
+import com.ning.billing.catalog.api.CatalogApiException;
+import com.ning.billing.catalog.api.Duration;
+import com.ning.billing.catalog.api.PhaseType;
+import com.ning.billing.catalog.api.Plan;
+import com.ning.billing.catalog.api.PlanAlignmentCreate;
+import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.PlanSpecifier;
+import com.ning.billing.catalog.api.PriceListSet;
+import com.ning.billing.catalog.api.ProductCategory;
+import com.ning.billing.entitlement.api.TestApiBase;
+import com.ning.billing.entitlement.api.ApiTestListener.NextEvent;
+import com.ning.billing.entitlement.api.user.Subscription.SubscriptionState;
+import com.ning.billing.entitlement.api.user.SubscriptionTransition.SubscriptionTransitionType;
+import com.ning.billing.entitlement.glue.MockEngineModuleSql;
+import com.ning.billing.util.clock.DefaultClock;
+
+public class TestUserApiAddOn extends TestApiBase {
+
+ @Override
+ public Injector getInjector() {
+ return Guice.createInjector(Stage.DEVELOPMENT, new MockEngineModuleSql());
+ }
+
+
+ @Test(enabled=true, groups={"slow"})
+ public void testCreateCancelAddon() {
+
+ try {
+ String baseProduct = "Shotgun";
+ BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ SubscriptionData baseSubscription = createSubscription(baseProduct, baseTerm, basePriceList);
+
+ String aoProduct = "Telescopic-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ SubscriptionData aoSubscription = createSubscription(aoProduct, aoTerm, aoPriceList);
+ assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+
+ DateTime now = clock.getUTCNow();
+ aoSubscription.cancel(now, false);
+
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ assertTrue(testListener.isCompleted(5000));
+
+ assertEquals(aoSubscription.getState(), SubscriptionState.CANCELLED);
+
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(enabled=true, groups={"slow"})
+ public void testCancelBPWthAddon() {
+ try {
+
+ String baseProduct = "Shotgun";
+ BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ SubscriptionData baseSubscription = createSubscription(baseProduct, baseTerm, basePriceList);
+
+ String aoProduct = "Telescopic-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ SubscriptionData aoSubscription = createSubscription(aoProduct, aoTerm, aoPriceList);
+
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Duration twoMonths = getDurationMonth(2);
+ clock.setDeltaFromReality(twoMonths, DAY_IN_MS);
+ assertTrue(testListener.isCompleted(5000));
+
+ // SET CTD TO CANCEL IN FUTURE
+ DateTime now = clock.getUTCNow();
+ Duration ctd = getDurationMonth(1);
+ DateTime newChargedThroughDate = DefaultClock.addDuration(now, ctd);
+ billingApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate);
+ baseSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(baseSubscription.getId());
+
+ // FUTURE CANCELLATION
+ baseSubscription.cancel(now, false);
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+
+ // MOVE AFTER CANCELLATION
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ clock.addDeltaFromReality(ctd);
+ now = clock.getUTCNow();
+ assertTrue(testListener.isCompleted(5000));
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS CANCELLED
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ assertEquals(aoSubscription.getState(), SubscriptionState.CANCELLED);
+
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+
+ @Test(enabled=true, groups={"slow"})
+ public void testChangeBPWthAddonNonIncluded() {
+ try {
+
+ String baseProduct = "Shotgun";
+ BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ SubscriptionData baseSubscription = createSubscription(baseProduct, baseTerm, basePriceList);
+
+ String aoProduct = "Telescopic-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ SubscriptionData aoSubscription = createSubscription(aoProduct, aoTerm, aoPriceList);
+
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Duration twoMonths = getDurationMonth(2);
+ clock.setDeltaFromReality(twoMonths, DAY_IN_MS);
+ assertTrue(testListener.isCompleted(5000));
+
+ // SET CTD TO CHANGE IN FUTURE
+ DateTime now = clock.getUTCNow();
+ Duration ctd = getDurationMonth(1);
+ DateTime newChargedThroughDate = DefaultClock.addDuration(now, ctd);
+ billingApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate);
+ baseSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(baseSubscription.getId());
+
+ // CHANGE IMMEDIATELY WITH TO BP WITH NON INCLUDED ADDON
+ String newBaseProduct = "Assault-Rifle";
+ BillingPeriod newBaseTerm = BillingPeriod.MONTHLY;
+ String newBasePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, now);
+ assertTrue(testListener.isCompleted(5000));
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ assertEquals(aoSubscription.getState(), SubscriptionState.CANCELLED);
+
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(enabled=true, groups={"slow"})
+ public void testChangeBPWthAddonNonAvailable() {
+ try {
+
+ String baseProduct = "Shotgun";
+ BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ SubscriptionData baseSubscription = createSubscription(baseProduct, baseTerm, basePriceList);
+
+ String aoProduct = "Telescopic-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ SubscriptionData aoSubscription = createSubscription(aoProduct, aoTerm, aoPriceList);
+
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE CLOCK AFTER TRIAL + AO DISCOUNT
+ Duration twoMonths = getDurationMonth(2);
+ clock.setDeltaFromReality(twoMonths, DAY_IN_MS);
+ assertTrue(testListener.isCompleted(5000));
+
+ // SET CTD TO CANCEL IN FUTURE
+ DateTime now = clock.getUTCNow();
+ Duration ctd = getDurationMonth(1);
+ DateTime newChargedThroughDate = DefaultClock.addDuration(now, ctd);
+ billingApi.setChargedThroughDate(baseSubscription.getId(), newChargedThroughDate);
+ baseSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(baseSubscription.getId());
+
+ // CHANGE IMMEDIATELY WITH TO BP WITH NON AVAILABLE ADDON
+ String newBaseProduct = "Pistol";
+ BillingPeriod newBaseTerm = BillingPeriod.MONTHLY;
+ String newBasePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ baseSubscription.changePlan(newBaseProduct, newBaseTerm, newBasePriceList, now);
+
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS IS ACTIVE
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ assertEquals(aoSubscription.getState(), SubscriptionState.ACTIVE);
+ assertTrue(aoSubscription.isSubscriptionFutureCancelled());
+
+ // MOVE AFTER CHANGE
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ testListener.pushExpectedEvent(NextEvent.CANCEL);
+ clock.addDeltaFromReality(ctd);
+ assertTrue(testListener.isCompleted(5000));
+
+
+ // REFETCH AO SUBSCRIPTION AND CHECK THIS CANCELLED
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ assertEquals(aoSubscription.getState(), SubscriptionState.CANCELLED);
+
+ } catch (Exception e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+
+ @Test(enabled=true, groups={"slow"})
+ public void testAddonCreateWithBundleAlign() {
+ try {
+ String aoProduct = "Telescopic-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // This is just to double check our test catalog gives us what we want before we start the test
+ PlanSpecifier planSpecifier = new PlanSpecifier(aoProduct,
+ ProductCategory.ADD_ON,
+ aoTerm,
+ aoPriceList);
+ PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, clock.getUTCNow());
+ assertEquals(alignement, PlanAlignmentCreate.START_OF_BUNDLE);
+
+ testAddonCreateInternal(aoProduct, aoTerm, aoPriceList, alignement);
+
+ } catch (CatalogApiException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+ @Test(enabled=true, groups={"slow"})
+ public void testAddonCreateWithSubscriptionAlign() {
+
+ try {
+ String aoProduct = "Laser-Scope";
+ BillingPeriod aoTerm = BillingPeriod.MONTHLY;
+ String aoPriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // This is just to double check our test catalog gives us what we want before we start the test
+ PlanSpecifier planSpecifier = new PlanSpecifier(aoProduct,
+ ProductCategory.ADD_ON,
+ aoTerm,
+ aoPriceList);
+ PlanAlignmentCreate alignement = catalog.planCreateAlignment(planSpecifier, clock.getUTCNow());
+ assertEquals(alignement, PlanAlignmentCreate.START_OF_SUBSCRIPTION);
+
+ testAddonCreateInternal(aoProduct, aoTerm, aoPriceList, alignement);
+
+ } catch (CatalogApiException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
+
+ private void testAddonCreateInternal(String aoProduct, BillingPeriod aoTerm, String aoPriceList, PlanAlignmentCreate expAlignement) {
+ try {
+
+ String baseProduct = "Shotgun";
+ BillingPeriod baseTerm = BillingPeriod.MONTHLY;
+ String basePriceList = PriceListSet.DEFAULT_PRICELIST_NAME;
+
+ // CREATE BP
+ SubscriptionData baseSubscription = createSubscription(baseProduct, baseTerm, basePriceList);
+
+ // MOVE CLOCK 14 DAYS LATER
+ Duration someTimeLater = getDurationDay(13);
+ clock.setDeltaFromReality(someTimeLater, DAY_IN_MS);
+
+ // CREATE ADDON
+ DateTime beforeAOCreation = clock.getUTCNow();
+ SubscriptionData aoSubscription = createSubscription(aoProduct, aoTerm, aoPriceList);
+ DateTime afterAOCreation = clock.getUTCNow();
+
+ // CHECK EVERYTHING
+ Plan aoCurrentPlan = aoSubscription.getCurrentPlan();
+ assertNotNull(aoCurrentPlan);
+ assertEquals(aoCurrentPlan.getProduct().getName(),aoProduct);
+ assertEquals(aoCurrentPlan.getProduct().getCategory(), ProductCategory.ADD_ON);
+ assertEquals(aoCurrentPlan.getBillingPeriod(), aoTerm);
+
+ PlanPhase aoCurrentPhase = aoSubscription.getCurrentPhase();
+ assertNotNull(aoCurrentPhase);
+ assertEquals(aoCurrentPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ assertDateWithin(aoSubscription.getStartDate(), beforeAOCreation, afterAOCreation);
+ assertEquals(aoSubscription.getBundleStartDate(), baseSubscription.getBundleStartDate());
+
+ // CHECK next AO PHASE EVENT IS INDEED A MONTH AFTER BP STARTED => BUNDLE ALIGNMENT
+ SubscriptionTransition aoPendingTranstion = aoSubscription.getPendingTransition();
+
+ if (expAlignement == PlanAlignmentCreate.START_OF_BUNDLE) {
+ assertEquals(aoPendingTranstion.getEffectiveTransitionTime(), baseSubscription.getStartDate().plusMonths(1));
+ } else {
+ assertEquals(aoPendingTranstion.getEffectiveTransitionTime(), aoSubscription.getStartDate().plusMonths(1));
+ }
+
+ // ADD TWO PHASE EVENTS (BP + AO)
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+
+ // MOVE THROUGH TIME TO GO INTO EVERGREEN
+ someTimeLater = aoCurrentPhase.getDuration();
+ clock.addDeltaFromReality(someTimeLater);
+ assertTrue(testListener.isCompleted(5000));
+
+
+ // CHECK EVERYTHING AGAIN
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+
+ aoCurrentPlan = aoSubscription.getCurrentPlan();
+ assertNotNull(aoCurrentPlan);
+ assertEquals(aoCurrentPlan.getProduct().getName(),aoProduct);
+ assertEquals(aoCurrentPlan.getProduct().getCategory(), ProductCategory.ADD_ON);
+ assertEquals(aoCurrentPlan.getBillingPeriod(), aoTerm);
+
+ aoCurrentPhase = aoSubscription.getCurrentPhase();
+ assertNotNull(aoCurrentPhase);
+ assertEquals(aoCurrentPhase.getPhaseType(), PhaseType.EVERGREEN);
+
+
+ aoSubscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(aoSubscription.getId());
+ aoPendingTranstion = aoSubscription.getPendingTransition();
+ assertNull(aoPendingTranstion);
+
+ } catch (EntitlementUserApiException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
index 840f357..469d374 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCancelSql.java
@@ -49,25 +49,25 @@ public class TestUserApiCancelSql extends TestUserApiCancel {
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testCancelSubscriptionIMM() {
super.testCancelSubscriptionIMM();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testCancelSubscriptionEOTWithChargeThroughDate() throws EntitlementBillingApiException {
super.testCancelSubscriptionEOTWithChargeThroughDate();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testCancelSubscriptionEOTWithNoChargeThroughDate() {
super.testCancelSubscriptionEOTWithNoChargeThroughDate();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testUncancel() throws EntitlementBillingApiException {
super.testUncancel();
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
index 78616be..4505ef0 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlan.java
@@ -428,4 +428,54 @@ public abstract class TestUserApiChangePlan extends TestApiBase {
}
}
+
+ protected void testCorrectPhaseAlignmentOnChange() {
+ try {
+
+ SubscriptionData subscription = createSubscription("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ PlanPhase trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE 2 DAYS AHEAD
+ clock.setDeltaFromReality(getDurationDay(1), DAY_IN_MS);
+
+ // CHANGE IMMEDIATE TO A 3 PHASES PLAN
+ testListener.reset();
+ testListener.pushExpectedEvent(NextEvent.CHANGE);
+ subscription.changePlan("Assault-Rifle", BillingPeriod.ANNUAL, "gunclubDiscount", clock.getUTCNow());
+ assertTrue(testListener.isCompleted(3000));
+ testListener.reset();
+
+ // CHECK EVERYTHING LOOKS CORRECT
+ Plan currentPlan = subscription.getCurrentPlan();
+ assertNotNull(currentPlan);
+ assertEquals(currentPlan.getProduct().getName(), "Assault-Rifle");
+ assertEquals(currentPlan.getProduct().getCategory(), ProductCategory.BASE);
+ assertEquals(currentPlan.getBillingPeriod(), BillingPeriod.ANNUAL);
+
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.TRIAL);
+
+ // MOVE AFTER TRIAL PERIOD -> DISCOUNT
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.addDeltaFromReality(trialPhase.getDuration());
+ assertTrue(testListener.isCompleted(3000));
+
+ trialPhase = subscription.getCurrentPhase();
+ assertEquals(trialPhase.getPhaseType(), PhaseType.DISCOUNT);
+
+ subscription = (SubscriptionData) entitlementApi.getSubscriptionFromId(subscription.getId());
+
+ DateTime expectedNextPhaseDate = subscription.getStartDate().plusDays(30).plusMonths(6);
+ SubscriptionTransition nextPhase = subscription.getPendingTransition();
+ DateTime nextPhaseEffectiveDate = nextPhase.getEffectiveTransitionTime();
+
+ assertEquals(nextPhaseEffectiveDate, expectedNextPhaseDate);
+
+
+ } catch (EntitlementUserApiException e) {
+ Assert.fail(e.getMessage());
+ }
+ }
+
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
index 253da07..03b9d91 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanMemory.java
@@ -67,4 +67,10 @@ public class TestUserApiChangePlanMemory extends TestUserApiChangePlan {
public void testChangePlanChangePlanAlignEOTWithChargeThroughDate() throws EntitlementBillingApiException {
super.testChangePlanChangePlanAlignEOTWithChargeThroughDate();
}
+
+ @Override
+ @Test(enabled=true, groups={"fast"})
+ public void testCorrectPhaseAlignmentOnChange() {
+ super.testCorrectPhaseAlignmentOnChange();
+ }
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
index 92aa652..735099c 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiChangePlanSql.java
@@ -54,38 +54,44 @@ public class TestUserApiChangePlanSql extends TestUserApiChangePlan {
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
+ public void testCorrectPhaseAlignmentOnChange() {
+ super.testCorrectPhaseAlignmentOnChange();
+ }
+
+ @Override
+ @Test(enabled=true, groups={"slow"})
public void testChangePlanBundleAlignEOTWithNoChargeThroughDate() {
super.testChangePlanBundleAlignEOTWithNoChargeThroughDate();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testChangePlanBundleAlignEOTWithChargeThroughDate() throws EntitlementBillingApiException {
super.testChangePlanBundleAlignEOTWithChargeThroughDate();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testChangePlanBundleAlignIMM() {
super.testChangePlanBundleAlignIMM();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testMultipleChangeLastIMM() throws EntitlementBillingApiException {
super.testMultipleChangeLastIMM();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testMultipleChangeLastEOT() throws EntitlementBillingApiException {
super.testMultipleChangeLastEOT();
}
// rescue not implemented yet
@Override
- @Test(enabled=false, groups={"sql"})
+ @Test(enabled=false, groups={"slow"})
public void testChangePlanChangePlanAlignEOTWithChargeThroughDate() throws EntitlementBillingApiException {
super.testChangePlanChangePlanAlignEOTWithChargeThroughDate();
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
index 8170b6d..6820fec 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiCreateSql.java
@@ -30,31 +30,31 @@ public class TestUserApiCreateSql extends TestUserApiCreate {
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testCreateWithRequestedDate() {
super.testCreateWithRequestedDate();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testCreateWithInitialPhase() {
super.testCreateWithInitialPhase();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
public void testSimpleCreateSubscription() {
super.testSimpleCreateSubscription();
}
@Override
- @Test(enabled=true, groups={"sql"})
+ @Test(enabled=true, groups={"slow"})
protected void testSimpleSubscriptionThroughPhases() {
super.testSimpleSubscriptionThroughPhases();
}
@Override
- @Test(enabled=false, groups={"sql"})
+ @Test(enabled=false, groups={"slow"})
protected void testSubscriptionWithAddOn() {
super.testSubscriptionWithAddOn();
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
index e485420..81ee501 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/api/user/TestUserApiError.java
@@ -36,6 +36,7 @@ import java.util.UUID;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
public class TestUserApiError extends TestApiBase {
@@ -46,7 +47,7 @@ public class TestUserApiError extends TestApiBase {
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testCreateSubscriptionBadCatalog() {
// WRONG PRODUTCS
tCreateSubscriptionInternal(bundle.getId(), null, BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.CAT_NULL_PRODUCT_NAME);
@@ -62,17 +63,17 @@ public class TestUserApiError extends TestApiBase {
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testCreateSubscriptionNoBundle() {
tCreateSubscriptionInternal(null, "Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.ENT_CREATE_NO_BUNDLE);
}
- @Test(enabled=false)
+ @Test(enabled=true, groups={"fast"})
public void testCreateSubscriptionNoBP() {
- //tCreateSubscriptionInternal(bundle.getId(), "Shotgun", BillingPeriod.ANNUAL, IPriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.ENT_CREATE_NO_BP);
+ tCreateSubscriptionInternal(bundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.ENT_CREATE_NO_BP);
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testCreateSubscriptionBPExists() {
try {
createSubscription("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
@@ -83,6 +84,33 @@ public class TestUserApiError extends TestApiBase {
}
}
+ @Test(enabled=true, groups={"fast"})
+ public void testCreateSubscriptionAddOnNotAvailable() {
+ try {
+ UUID accountId = UUID.randomUUID();
+ SubscriptionBundle aoBundle = entitlementApi.createBundleForAccount(accountId, "myAOBundle");
+ createSubscriptionWithBundle(aoBundle.getId(), "Pistol", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.ENT_CREATE_AO_NOT_AVAILABLE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ Assert.assertFalse(true);
+ }
+ }
+
+ @Test(enabled=true, groups={"fast"})
+ public void testCreateSubscriptionAddOnIncluded() {
+ try {
+ UUID accountId = UUID.randomUUID();
+ SubscriptionBundle aoBundle = entitlementApi.createBundleForAccount(accountId, "myAOBundle");
+ createSubscriptionWithBundle(aoBundle.getId(), "Assault-Rifle", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ tCreateSubscriptionInternal(aoBundle.getId(), "Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, ErrorCode.ENT_CREATE_AO_ALREADY_INCLUDED);
+ } catch (Exception e) {
+ e.printStackTrace();
+ Assert.assertFalse(true);
+ }
+ }
+
+
private void tCreateSubscriptionInternal(UUID bundleId, String productName,
BillingPeriod term, String planSet, ErrorCode expected) {
try {
@@ -101,7 +129,7 @@ public class TestUserApiError extends TestApiBase {
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testChangeSubscriptionNonActive() {
try {
Subscription subscription = createSubscription("Shotgun", BillingPeriod.ANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME);
@@ -125,17 +153,25 @@ public class TestUserApiError extends TestApiBase {
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testChangeSubscriptionFutureCancelled() {
try {
Subscription subscription = createSubscription("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
+ PlanPhase trialPhase = subscription.getCurrentPhase();
+
+ // MOVE TO NEXT PHASE
+ PlanPhase currentPhase = subscription.getCurrentPhase();
+ testListener.pushExpectedEvent(NextEvent.PHASE);
+ clock.setDeltaFromReality(currentPhase.getDuration(), DAY_IN_MS);
+ assertTrue(testListener.isCompleted(3000));
+
// SET CTD TO CANCEL IN FUTURE
- PlanPhase trialPhase = subscription.getCurrentPhase();
DateTime expectedPhaseTrialChange = DefaultClock.addDuration(subscription.getStartDate(), trialPhase.getDuration());
Duration ctd = getDurationMonth(1);
DateTime newChargedThroughDate = DefaultClock.addDuration(expectedPhaseTrialChange, ctd);
billingApi.setChargedThroughDate(subscription.getId(), newChargedThroughDate);
+
subscription = entitlementApi.getSubscriptionFromId(subscription.getId());
subscription.cancel(clock.getUTCNow(), false);
@@ -156,11 +192,11 @@ public class TestUserApiError extends TestApiBase {
}
- @Test(enabled=false)
+ @Test(enabled=false, groups={"fast"})
public void testCancelBadState() {
}
- @Test(enabled=true)
+ @Test(enabled=true, groups={"fast"})
public void testUncancelBadState() {
try {
Subscription subscription = createSubscription("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME);
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
index c5881f9..cb87dc0 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/engine/dao/MockEntitlementDaoSql.java
@@ -25,6 +25,7 @@ import org.skife.jdbi.v2.sqlobject.mixins.Transactional;
import com.google.inject.Inject;
import com.ning.billing.entitlement.api.user.SubscriptionFactory;
+import com.ning.billing.entitlement.engine.addon.AddonUtils;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.notificationq.NotificationQueueService;
@@ -33,8 +34,8 @@ public class MockEntitlementDaoSql extends EntitlementSqlDao implements MockEnti
private final ResetSqlDao resetDao;
@Inject
- public MockEntitlementDaoSql(IDBI dbi, Clock clock, SubscriptionFactory factory, NotificationQueueService notificationQueueService) {
- super(dbi, clock, factory, notificationQueueService);
+ public MockEntitlementDaoSql(IDBI dbi, Clock clock, SubscriptionFactory factory, AddonUtils addonUtils, NotificationQueueService notificationQueueService) {
+ super(dbi, clock, factory, addonUtils, notificationQueueService);
this.resetDao = dbi.onDemand(ResetSqlDao.class);
}
diff --git a/entitlement/src/test/java/com/ning/billing/entitlement/glue/MockEngineModuleSql.java b/entitlement/src/test/java/com/ning/billing/entitlement/glue/MockEngineModuleSql.java
index e9e6134..c452c3b 100644
--- a/entitlement/src/test/java/com/ning/billing/entitlement/glue/MockEngineModuleSql.java
+++ b/entitlement/src/test/java/com/ning/billing/entitlement/glue/MockEngineModuleSql.java
@@ -18,6 +18,7 @@ package com.ning.billing.entitlement.glue;
import com.ning.billing.dbi.DBIProvider;
import com.ning.billing.dbi.DbiConfig;
+import com.ning.billing.dbi.MysqlTestingHelper;
import com.ning.billing.entitlement.engine.dao.EntitlementDao;
import com.ning.billing.entitlement.engine.dao.MockEntitlementDaoSql;
import com.ning.billing.util.clock.Clock;
@@ -36,9 +37,16 @@ public class MockEngineModuleSql extends MockEngineModule {
}
protected void installDBI() {
- bind(IDBI.class).toProvider(DBIProvider.class).asEagerSingleton();
- final DbiConfig config = new ConfigurationObjectFactory(System.getProperties()).build(DbiConfig.class);
- bind(DbiConfig.class).toInstance(config);
+ final MysqlTestingHelper helper = new MysqlTestingHelper();
+ bind(MysqlTestingHelper.class).toInstance(helper);
+ if (helper.isUsingLocalInstance()) {
+ bind(IDBI.class).toProvider(DBIProvider.class).asEagerSingleton();
+ final DbiConfig config = new ConfigurationObjectFactory(System.getProperties()).build(DbiConfig.class);
+ bind(DbiConfig.class).toInstance(config);
+ } else {
+ final IDBI dbi = helper.getDBI();
+ bind(IDBI.class).toInstance(dbi);
+ }
}
@Override
diff --git a/entitlement/src/test/resources/entitlement.properties b/entitlement/src/test/resources/entitlement.properties
index d149d78..af1c3fc 100644
--- a/entitlement/src/test/resources/entitlement.properties
+++ b/entitlement/src/test/resources/entitlement.properties
@@ -2,5 +2,5 @@ killbill.catalog.uri=file:src/test/resources/testInput.xml
killbill.entitlement.dao.claim.time=60000
killbill.entitlement.dao.ready.max=1
killbill.entitlement.engine.notifications.sleep=500
-
+user.timezone=UTC
entitlement/src/test/resources/testInput.xml 91(+54 -37)
diff --git a/entitlement/src/test/resources/testInput.xml b/entitlement/src/test/resources/testInput.xml
index ce3b250..8a97d57 100644
--- a/entitlement/src/test/resources/testInput.xml
+++ b/entitlement/src/test/resources/testInput.xml
@@ -51,13 +51,13 @@ Use Cases to do:
<products>
<product name="Pistol">
<category>BASE</category>
- <available>
- <addonProduct>Telescopic-Scope</addonProduct>
- <addonProduct>Laser-Scope</addonProduct>
- </available>
</product>
<product name="Shotgun">
<category>BASE</category>
+ <available>
+ <addonProduct>Telescopic-Scope</addonProduct>
+ <addonProduct>Laser-Scope</addonProduct>
+ </available>
</product>
<product name="Assault-Rifle">
<category>BASE</category>
@@ -91,33 +91,18 @@ Use Cases to do:
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</changePolicyCase>
- <changePolicyCase>
- <toProduct>Pistol</toProduct>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
+ <changePolicyCase>
+ <toProduct>Assault-Rifle</toProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ <changePolicyCase>
+ <fromProduct>Pistol</fromProduct>
+ <toProduct>Shotgun</toProduct>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
<changePolicyCase>
<toPriceList>rescue</toPriceList>
<policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromProduct>Pistol</fromProduct>
- <toProduct>Shotgun</toProduct>
- <policy>IMMEDIATE</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromProduct>Assault-Rifle</fromProduct>
- <toProduct>Shotgun</toProduct>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <fromBillingPeriod>MONTHLY</fromBillingPeriod>
- <toProduct>Assault-Rifle</toProduct>
- <toBillingPeriod>MONTHLY</toBillingPeriod>
- <policy>END_OF_TERM</policy>
- </changePolicyCase>
- <changePolicyCase>
- <toProduct>Assault-Rifle</toProduct>
- <policy>IMMEDIATE</policy>
</changePolicyCase>
<changePolicyCase>
<fromBillingPeriod>MONTHLY</fromBillingPeriod>
@@ -135,9 +120,6 @@ Use Cases to do:
</changePolicy>
<changeAlignment>
<changeAlignmentCase>
- <alignment>START_OF_SUBSCRIPTION</alignment>
- </changeAlignmentCase>
- <changeAlignmentCase>
<toPriceList>rescue</toPriceList>
<alignment>CHANGE_OF_PLAN</alignment>
</changeAlignmentCase>
@@ -146,20 +128,27 @@ Use Cases to do:
<toPriceList>rescue</toPriceList>
<alignment>CHANGE_OF_PRICELIST</alignment>
</changeAlignmentCase>
+ <changeAlignmentCase>
+ <alignment>START_OF_SUBSCRIPTION</alignment>
+ </changeAlignmentCase>
</changeAlignment>
<cancelPolicy>
<cancelPolicyCase>
- <policy>END_OF_TERM</policy>
- </cancelPolicyCase>
- <cancelPolicyCase>
<phaseType>TRIAL</phaseType>
<policy>IMMEDIATE</policy>
</cancelPolicyCase>
+ <cancelPolicyCase>
+ <policy>END_OF_TERM</policy>
+ </cancelPolicyCase>
</cancelPolicy>
<createAlignment>
- <createAlignmentCase>
- <alignment>START_OF_BUNDLE</alignment>
- </createAlignmentCase>
+ <createAlignmentCase>
+ <product>Laser-Scope</product>
+ <alignment>START_OF_SUBSCRIPTION</alignment>
+ </createAlignmentCase>
+ <createAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </createAlignmentCase>
</createAlignment>
<billingAlignment>
<billingAlignmentCase>
@@ -447,6 +436,20 @@ Use Cases to do:
</plan>
<plan name="laser-scope-monthly">
<product>Laser-Scope</product>
+ <initialPhases>
+ <phase type="DISCOUNT">
+ <duration>
+ <unit>MONTHS</unit>
+ <number>1</number>
+ </duration>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price><currency>USD</currency><value>999.95</value></price>
+ <price><currency>EUR</currency><value>499.95</value></price>
+ <price><currency>GBP</currency><value>999.95</value></price>
+ </recurringPrice>
+ </phase>
+ </initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
@@ -461,6 +464,20 @@ Use Cases to do:
</plan>
<plan name="telescopic-scope-monthly">
<product>Telescopic-Scope</product>
+ <initialPhases>
+ <phase type="DISCOUNT">
+ <duration>
+ <unit>MONTHS</unit>
+ <number>1</number>
+ </duration>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price><currency>USD</currency><value>399.95</value></price>
+ <price><currency>EUR</currency><value>299.95</value></price>
+ <price><currency>GBP</currency><value>399.95</value></price>
+ </recurringPrice>
+ </phase>
+ </initialPhases>
<finalPhase type="EVERGREEN">
<duration>
<unit>UNLIMITED</unit>
diff --git a/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java b/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
index 7a9253c..c0b054a 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/dao/MockSubscription.java
@@ -19,6 +19,7 @@ package com.ning.billing.invoice.dao;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -124,4 +125,9 @@ public class MockSubscription implements Subscription {
public SubscriptionTransition getPreviousTransition() {
return null;
}
+
+ @Override
+ public ProductCategory getCategory() {
+ throw new UnsupportedOperationException();
+ }
}
\ No newline at end of file
diff --git a/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java b/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
index 587144b..05d7c79 100644
--- a/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
+++ b/invoice/src/test/java/com/ning/billing/invoice/notification/BrainDeadSubscription.java
@@ -24,6 +24,7 @@ import org.joda.time.DateTime;
import com.ning.billing.catalog.api.BillingPeriod;
import com.ning.billing.catalog.api.Plan;
import com.ning.billing.catalog.api.PlanPhase;
+import com.ning.billing.catalog.api.ProductCategory;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionTransition;
@@ -47,103 +48,108 @@ public class BrainDeadSubscription implements Subscription {
throws EntitlementUserApiException {
throw new UnsupportedOperationException();
-
+
}
@Override
public void pause() throws EntitlementUserApiException {
throw new UnsupportedOperationException();
-
+
}
@Override
public void resume() throws EntitlementUserApiException {
throw new UnsupportedOperationException();
-
+
}
@Override
public UUID getId() {
throw new UnsupportedOperationException();
-
+
}
@Override
public UUID getBundleId() {
throw new UnsupportedOperationException();
-
+
}
@Override
public SubscriptionState getState() {
throw new UnsupportedOperationException();
-
+
}
@Override
public DateTime getStartDate() {
throw new UnsupportedOperationException();
-
+
}
@Override
public DateTime getEndDate() {
throw new UnsupportedOperationException();
-
+
}
@Override
public Plan getCurrentPlan() {
throw new UnsupportedOperationException();
-
+
}
@Override
public String getCurrentPriceList() {
throw new UnsupportedOperationException();
-
+
}
@Override
public PlanPhase getCurrentPhase() {
throw new UnsupportedOperationException();
-
+
}
@Override
public DateTime getChargedThroughDate() {
throw new UnsupportedOperationException();
-
+
}
@Override
public DateTime getPaidThroughDate() {
throw new UnsupportedOperationException();
-
+
}
@Override
public List<SubscriptionTransition> getActiveTransitions() {
throw new UnsupportedOperationException();
-
+
}
@Override
public List<SubscriptionTransition> getAllTransitions() {
throw new UnsupportedOperationException();
-
+
}
@Override
public SubscriptionTransition getPendingTransition() {
throw new UnsupportedOperationException();
-
+
}
@Override
public SubscriptionTransition getPreviousTransition() {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException();
}
+ @Override
+ public ProductCategory getCategory() {
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index 3b1a6f9..4dd5040 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -25,7 +25,6 @@ import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
-import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +34,7 @@ import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoicePaymentApi;
import com.ning.billing.invoice.model.DefaultInvoicePayment;
+import com.ning.billing.payment.RetryService;
import com.ning.billing.payment.dao.PaymentDao;
import com.ning.billing.payment.provider.PaymentProviderPlugin;
import com.ning.billing.payment.provider.PaymentProviderPluginRegistry;
@@ -44,6 +44,7 @@ public class DefaultPaymentApi implements PaymentApi {
private final PaymentProviderPluginRegistry pluginRegistry;
private final AccountUserApi accountUserApi;
private final InvoicePaymentApi invoicePaymentApi;
+ private final RetryService retryService;
private final PaymentDao paymentDao;
private final PaymentConfig config;
@@ -53,11 +54,13 @@ public class DefaultPaymentApi implements PaymentApi {
public DefaultPaymentApi(PaymentProviderPluginRegistry pluginRegistry,
AccountUserApi accountUserApi,
InvoicePaymentApi invoicePaymentApi,
+ RetryService retryService,
PaymentDao paymentDao,
PaymentConfig config) {
this.pluginRegistry = pluginRegistry;
this.accountUserApi = accountUserApi;
this.invoicePaymentApi = invoicePaymentApi;
+ this.retryService = retryService;
this.paymentDao = paymentDao;
this.config = config;
}
@@ -134,7 +137,7 @@ public class DefaultPaymentApi implements PaymentApi {
}
@Override
- public Either<PaymentError, PaymentInfo> createPayment(UUID paymentAttemptId) {
+ public Either<PaymentError, PaymentInfo> createPaymentForPaymentAttempt(UUID paymentAttemptId) {
PaymentAttempt paymentAttempt = paymentDao.getPaymentAttemptById(paymentAttemptId);
if (paymentAttempt != null) {
@@ -142,17 +145,23 @@ public class DefaultPaymentApi implements PaymentApi {
Account account = accountUserApi.getAccountById(paymentAttempt.getAccountId());
if (invoice != null && account != null) {
- if (invoice.getBalance().compareTo(BigDecimal.ZERO) == 0 ) {
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0 ) {
// TODO: send a notification that invoice was ignored?
log.info("Received invoice for payment with outstanding amount of 0 {} ", invoice);
- Either.left(new PaymentError("invoice_balance_0", "Invoice balance was 0"));
+ return Either.left(new PaymentError("invoice_balance_0",
+ "Invoice balance was 0 or less",
+ paymentAttempt.getAccountId(),
+ paymentAttempt.getInvoiceId()));
}
else {
return processPayment(getPaymentProviderPlugin(account), account, invoice, paymentAttempt);
}
}
}
- return Either.left(new PaymentError("retry_payment_error", "Could not load payment attempt, invoice or account for id " + paymentAttemptId));
+ return Either.left(new PaymentError("retry_payment_error",
+ "Could not load payment attempt, invoice or account for id " + paymentAttemptId,
+ paymentAttempt.getAccountId(),
+ paymentAttempt.getInvoiceId()));
}
@Override
@@ -164,10 +173,14 @@ public class DefaultPaymentApi implements PaymentApi {
for (String invoiceId : invoiceIds) {
Invoice invoice = invoicePaymentApi.getInvoice(UUID.fromString(invoiceId));
- if (invoice.getBalance().compareTo(BigDecimal.ZERO) == 0 ) {
+ if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0 ) {
// TODO: send a notification that invoice was ignored?
log.info("Received invoice for payment with balance of 0 {} ", invoice);
- Either.left(new PaymentError("invoice_balance_0", "Invoice balance was 0"));
+ Either<PaymentError, PaymentInfo> result = Either.left(new PaymentError("invoice_balance_0",
+ "Invoice balance was 0 or less",
+ account.getId(),
+ UUID.fromString(invoiceId)));
+ processedPaymentsOrErrors.add(result);
}
else {
PaymentAttempt paymentAttempt = paymentDao.createPaymentAttempt(invoice);
@@ -234,7 +247,7 @@ public class DefaultPaymentApi implements PaymentApi {
if (retryCount < retryDays.size()) {
int retryInDays = 0;
- DateTime nextRetryDate = new DateTime(DateTimeZone.UTC);
+ DateTime nextRetryDate = paymentAttempt.getPaymentAttemptDate();
try {
retryInDays = retryDays.get(retryCount);
@@ -244,6 +257,7 @@ public class DefaultPaymentApi implements PaymentApi {
log.error("Could not get retry day for retry count {}", retryCount);
}
+ retryService.scheduleRetry(paymentAttempt, nextRetryDate);
paymentDao.updatePaymentAttemptWithRetryInfo(paymentAttempt.getPaymentAttemptId(), retryCount + 1, nextRetryDate);
}
else if (retryCount == retryDays.size()) {
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
index b505fa0..49a4165 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
@@ -20,14 +20,15 @@ import java.util.Date;
import java.util.List;
import java.util.UUID;
-import com.ning.billing.util.clock.Clock;
import org.joda.time.DateTime;
import org.skife.jdbi.v2.IDBI;
+import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.payment.api.PaymentAttempt;
import com.ning.billing.payment.api.PaymentInfo;
+import com.ning.billing.util.clock.Clock;
public class DefaultPaymentDao implements PaymentDao {
private final PaymentSqlDao sqlDao;
@@ -80,16 +81,25 @@ public class DefaultPaymentDao implements PaymentDao {
@Override
public List<PaymentInfo> getPaymentInfo(List<String> invoiceIds) {
- return sqlDao.getPaymentInfos(invoiceIds);
+ if (invoiceIds == null || invoiceIds.size() == 0) {
+ return ImmutableList.<PaymentInfo>of();
+ } else {
+ return sqlDao.getPaymentInfos(invoiceIds);
+ }
}
@Override
public List<PaymentAttempt> getPaymentAttemptsForInvoiceIds(List<String> invoiceIds) {
- return sqlDao.getPaymentAttemptsForInvoiceIds(invoiceIds);
+ if (invoiceIds == null || invoiceIds.size() == 0) {
+ return ImmutableList.<PaymentAttempt>of();
+ } else {
+ return sqlDao.getPaymentAttemptsForInvoiceIds(invoiceIds);
+ }
}
@Override
public void updatePaymentAttemptWithRetryInfo(UUID paymentAttemptId, int retryCount, DateTime nextRetryDate) {
+
final Date retryDate = nextRetryDate == null ? null : nextRetryDate.toDate();
sqlDao.updatePaymentAttemptWithRetryInfo(paymentAttemptId.toString(), retryCount, retryDate, clock.getUTCNow().toDate());
}
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPlugin.java
index 71c7e41..280d1e0 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/NoOpPaymentProviderPlugin.java
@@ -53,7 +53,10 @@ public class NoOpPaymentProviderPlugin implements PaymentProviderPlugin {
@Override
public Either<PaymentError, String> createPaymentProviderAccount(Account account) {
- return Either.left(new PaymentError("unsupported", "Account creation not supported in this plugin"));
+ return Either.left(new PaymentError("unsupported",
+ "Account creation not supported in this plugin",
+ account.getId(),
+ null));
}
@Override
diff --git a/payment/src/main/java/com/ning/billing/payment/RetryService.java b/payment/src/main/java/com/ning/billing/payment/RetryService.java
index bbe8a61..ee57397 100644
--- a/payment/src/main/java/com/ning/billing/payment/RetryService.java
+++ b/payment/src/main/java/com/ning/billing/payment/RetryService.java
@@ -19,7 +19,6 @@ package com.ning.billing.payment;
import java.util.UUID;
import org.joda.time.DateTime;
-import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
import com.google.inject.Inject;
import com.ning.billing.lifecycle.KillbillService;
@@ -82,7 +81,7 @@ public class RetryService implements KillbillService {
}
}
- public void scheduleRetry(Transmogrifier transactionalDao, PaymentAttempt paymentAttempt, DateTime timeOfRetry) {
+ public void scheduleRetry(PaymentAttempt paymentAttempt, DateTime timeOfRetry) {
final String id = paymentAttempt.getPaymentAttemptId().toString();
NotificationKey key = new NotificationKey() {
@@ -91,14 +90,14 @@ public class RetryService implements KillbillService {
return id;
}
};
- retryQueue.recordFutureNotificationFromTransaction(transactionalDao, timeOfRetry, key);
+ retryQueue.recordFutureNotification(timeOfRetry, key);
}
private void retry(String paymentAttemptId) {
PaymentInfo paymentInfo = paymentApi.getPaymentInfoForPaymentAttemptId(paymentAttemptId);
- if (paymentInfo != null && !PaymentStatus.Processed.equals(PaymentStatus.valueOf(paymentInfo.getStatus()))) {
- paymentApi.createPayment(UUID.fromString(paymentAttemptId));
+ if (paymentInfo == null || !PaymentStatus.Processed.equals(PaymentStatus.valueOf(paymentInfo.getStatus()))) {
+ paymentApi.createPaymentForPaymentAttempt(UUID.fromString(paymentAttemptId));
}
}
}
diff --git a/payment/src/main/java/com/ning/billing/payment/setup/PaymentModule.java b/payment/src/main/java/com/ning/billing/payment/setup/PaymentModule.java
index 85641ed..d3d9320 100644
--- a/payment/src/main/java/com/ning/billing/payment/setup/PaymentModule.java
+++ b/payment/src/main/java/com/ning/billing/payment/setup/PaymentModule.java
@@ -64,5 +64,6 @@ public class PaymentModule extends AbstractModule {
bind(PaymentService.class).to(DefaultPaymentService.class).asEagerSingleton();
installPaymentProviderPlugins(paymentConfig);
installPaymentDao();
+ installRetryEngine();
}
}
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
index 375bcfd..dee2a28 100644
--- a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -22,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.lang.RandomStringUtils;
import org.joda.time.DateTime;
@@ -39,24 +40,34 @@ import com.ning.billing.payment.api.PaymentProviderAccount;
import com.ning.billing.payment.api.PaypalPaymentMethodInfo;
public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
+ private final AtomicBoolean makeNextInvoiceFail = new AtomicBoolean(false);
private final Map<String, PaymentInfo> payments = new ConcurrentHashMap<String, PaymentInfo>();
private final Map<String, PaymentProviderAccount> accounts = new ConcurrentHashMap<String, PaymentProviderAccount>();
private final Map<String, PaymentMethodInfo> paymentMethods = new ConcurrentHashMap<String, PaymentMethodInfo>();
+ public void makeNextInvoiceFail() {
+ makeNextInvoiceFail.set(true);
+ }
+
@Override
public Either<PaymentError, PaymentInfo> processInvoice(Account account, Invoice invoice) {
- PaymentInfo payment = new PaymentInfo.Builder().setPaymentId(UUID.randomUUID().toString())
- .setAmount(invoice.getBalance())
- .setStatus("Processed")
- .setBankIdentificationNumber("1234")
- .setCreatedDate(new DateTime())
- .setEffectiveDate(new DateTime())
- .setPaymentNumber("12345")
- .setReferenceId("12345")
- .setType("Electronic")
- .build();
- payments.put(payment.getPaymentId(), payment);
- return Either.right(payment);
+ if (makeNextInvoiceFail.getAndSet(false)) {
+ return Either.left(new PaymentError("unknown", "test error", account.getId(), invoice.getId()));
+ }
+ else {
+ PaymentInfo payment = new PaymentInfo.Builder().setPaymentId(UUID.randomUUID().toString())
+ .setAmount(invoice.getBalance())
+ .setStatus("Processed")
+ .setBankIdentificationNumber("1234")
+ .setCreatedDate(new DateTime())
+ .setEffectiveDate(new DateTime())
+ .setPaymentNumber("12345")
+ .setReferenceId("12345")
+ .setType("Electronic")
+ .build();
+ payments.put(payment.getPaymentId(), payment);
+ return Either.right(payment);
+ }
}
@Override
@@ -64,7 +75,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
PaymentInfo payment = payments.get(paymentId);
if (payment == null) {
- return Either.left(new PaymentError("notfound", "No payment found for id " + paymentId));
+ return Either.left(new PaymentError("notfound", "No payment found for id " + paymentId, null, null));
}
else {
return Either.right(payment);
@@ -83,7 +94,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
return Either.right(id);
}
else {
- return Either.left(new PaymentError("unknown", "Did not get account to create payment provider account"));
+ return Either.left(new PaymentError("unknown", "Did not get account to create payment provider account", null, null));
}
}
@@ -93,7 +104,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
return Either.right(accounts.get(accountKey));
}
else {
- return Either.left(new PaymentError("unknown", "Did not get account for accountKey " + accountKey));
+ return Either.left(new PaymentError("unknown", "Did not get account for accountKey " + accountKey, null, null));
}
}
@@ -125,7 +136,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
realPaymentMethod = new CreditCardPaymentMethodInfo.Builder(ccPaymentMethod).setId(paymentMethodId).build();
}
if (realPaymentMethod == null) {
- return Either.left(new PaymentError("unsupported", "Payment method " + paymentMethod.getType() + " not supported by the plugin"));
+ return Either.left(new PaymentError("unsupported", "Payment method " + paymentMethod.getType() + " not supported by the plugin", null, null));
}
else {
if (shouldBeDefault) {
@@ -136,11 +147,11 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
}
}
else {
- return Either.left(new PaymentError("noaccount", "Could not retrieve account for accountKey " + accountKey));
+ return Either.left(new PaymentError("noaccount", "Could not retrieve account for accountKey " + accountKey, null, null));
}
}
else {
- return Either.left(new PaymentError("unknown", "Could not create add payment method " + paymentMethod + " for " + accountKey));
+ return Either.left(new PaymentError("unknown", "Could not create add payment method " + paymentMethod + " for " + accountKey, null, null));
}
}
@@ -184,7 +195,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
realPaymentMethod = new CreditCardPaymentMethodInfo.Builder(ccPaymentMethod).build();
}
if (realPaymentMethod == null) {
- return Either.left(new PaymentError("unsupported", "Payment method " + paymentMethod.getType() + " not supported by the plugin"));
+ return Either.left(new PaymentError("unsupported", "Payment method " + paymentMethod.getType() + " not supported by the plugin", null, null));
}
else {
paymentMethods.put(paymentMethod.getId(), paymentMethod);
@@ -192,7 +203,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
}
}
else {
- return Either.left(new PaymentError("unknown", "Could not create add payment method " + paymentMethod + " for " + accountKey));
+ return Either.left(new PaymentError("unknown", "Could not create add payment method " + paymentMethod + " for " + accountKey, null, null));
}
}
@@ -202,11 +213,11 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
if (paymentMethodInfo != null) {
if (Boolean.FALSE.equals(paymentMethodInfo.getDefaultMethod()) || paymentMethodInfo.getDefaultMethod() == null) {
if (paymentMethods.remove(paymentMethodId) == null) {
- return Either.left(new PaymentError("unknown", "Did not get any result back"));
+ return Either.left(new PaymentError("unknown", "Did not get any result back", null, null));
}
}
else {
- return Either.left(new PaymentError("error", "Cannot delete default payment method"));
+ return Either.left(new PaymentError("error", "Cannot delete default payment method", null, null));
}
}
return Either.right(null);
@@ -215,7 +226,7 @@ public class MockPaymentProviderPlugin implements PaymentProviderPlugin {
@Override
public Either<PaymentError, PaymentMethodInfo> getPaymentMethodInfo(String paymentMethodId) {
if (paymentMethodId == null) {
- return Either.left(new PaymentError("unknown", "Could not retrieve payment method for paymentMethodId " + paymentMethodId));
+ return Either.left(new PaymentError("unknown", "Could not retrieve payment method for paymentMethodId " + paymentMethodId, null, null));
}
return Either.right(paymentMethods.get(paymentMethodId));
diff --git a/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithEmbeddedDb.java b/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithEmbeddedDb.java
index 31fdae3..97aa31e 100644
--- a/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithEmbeddedDb.java
+++ b/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithEmbeddedDb.java
@@ -16,7 +16,6 @@
package com.ning.billing.payment.setup;
-import com.ning.billing.util.bus.Bus;
import org.apache.commons.collections.MapUtils;
import com.google.common.collect.ImmutableMap;
@@ -24,8 +23,10 @@ import com.google.inject.Provider;
import com.ning.billing.entitlement.api.billing.EntitlementBillingApi;
import com.ning.billing.mock.BrainDeadProxyFactory;
import com.ning.billing.payment.provider.MockPaymentProviderPluginModule;
-import com.ning.billing.payment.setup.PaymentTestModuleWithMocks.MockProvider;
+import com.ning.billing.util.bus.Bus;
import com.ning.billing.util.bus.InMemoryBus;
+import com.ning.billing.util.notificationq.DefaultNotificationQueueService;
+import com.ning.billing.util.notificationq.NotificationQueueService;
public class PaymentTestModuleWithEmbeddedDb extends PaymentModule {
public static class MockProvider implements Provider<EntitlementBillingApi> {
@@ -33,9 +34,9 @@ public class PaymentTestModuleWithEmbeddedDb extends PaymentModule {
public EntitlementBillingApi get() {
return BrainDeadProxyFactory.createBrainDeadProxyFor(EntitlementBillingApi.class);
}
-
+
}
-
+
public PaymentTestModuleWithEmbeddedDb() {
super(MapUtils.toProperties(ImmutableMap.of("killbill.payment.provider.default", "my-mock")));
}
@@ -49,6 +50,7 @@ public class PaymentTestModuleWithEmbeddedDb extends PaymentModule {
protected void configure() {
super.configure();
bind(Bus.class).to(InMemoryBus.class).asEagerSingleton();
- bind(EntitlementBillingApi.class).toProvider( MockProvider.class );
+ bind(EntitlementBillingApi.class).toProvider(MockProvider.class);
+ bind(NotificationQueueService.class).to(DefaultNotificationQueueService.class).asEagerSingleton();
}
}
diff --git a/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithMocks.java b/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithMocks.java
index 3dfc637..c8f79bc 100644
--- a/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithMocks.java
+++ b/payment/src/test/java/com/ning/billing/payment/setup/PaymentTestModuleWithMocks.java
@@ -31,6 +31,8 @@ import com.ning.billing.payment.dao.PaymentDao;
import com.ning.billing.payment.provider.MockPaymentProviderPluginModule;
import com.ning.billing.util.bus.Bus;
import com.ning.billing.util.bus.InMemoryBus;
+import com.ning.billing.util.notificationq.MockNotificationQueueService;
+import com.ning.billing.util.notificationq.NotificationQueueService;
public class PaymentTestModuleWithMocks extends PaymentModule {
public static class MockProvider implements Provider<EntitlementBillingApi> {
@@ -38,12 +40,13 @@ public class PaymentTestModuleWithMocks extends PaymentModule {
public EntitlementBillingApi get() {
return BrainDeadProxyFactory.createBrainDeadProxyFor(EntitlementBillingApi.class);
}
-
+
}
-
-
+
+
public PaymentTestModuleWithMocks() {
- super(MapUtils.toProperties(ImmutableMap.of("killbill.payment.provider.default", "my-mock")));
+ super(MapUtils.toProperties(ImmutableMap.of("killbill.payment.provider.default", "my-mock",
+ "killbill.payment.engine.events.off", "false")));
}
@Override
@@ -65,5 +68,6 @@ public class PaymentTestModuleWithMocks extends PaymentModule {
bind(MockInvoiceDao.class).asEagerSingleton();
bind(InvoiceDao.class).to(MockInvoiceDao.class);
bind(EntitlementBillingApi.class).toProvider( MockProvider.class );
+ bind(NotificationQueueService.class).to(MockNotificationQueueService.class).asEagerSingleton();
}
}
diff --git a/payment/src/test/java/com/ning/billing/payment/TestPaymentInvoiceIntegration.java b/payment/src/test/java/com/ning/billing/payment/TestPaymentInvoiceIntegration.java
index c1e62d8..888e017 100644
--- a/payment/src/test/java/com/ning/billing/payment/TestPaymentInvoiceIntegration.java
+++ b/payment/src/test/java/com/ning/billing/payment/TestPaymentInvoiceIntegration.java
@@ -26,7 +26,6 @@ import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.Callable;
-import com.ning.billing.invoice.glue.InvoiceModuleWithMocks;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.skife.jdbi.v2.IDBI;
@@ -46,6 +45,7 @@ import com.ning.billing.account.glue.AccountModule;
import com.ning.billing.dbi.MysqlTestingHelper;
import com.ning.billing.invoice.api.Invoice;
import com.ning.billing.invoice.api.InvoicePaymentApi;
+import com.ning.billing.invoice.glue.InvoiceModuleWithMocks;
import com.ning.billing.payment.api.PaymentApi;
import com.ning.billing.payment.api.PaymentAttempt;
import com.ning.billing.payment.api.PaymentError;
@@ -54,6 +54,7 @@ import com.ning.billing.payment.setup.PaymentTestModuleWithEmbeddedDb;
import com.ning.billing.util.bus.Bus;
import com.ning.billing.util.bus.Bus.EventBusException;
import com.ning.billing.util.clock.MockClockModule;
+import com.ning.billing.util.notificationq.NotificationQueueService;
public class TestPaymentInvoiceIntegration {
// create payment for received invoice and save it -- positive and negative
@@ -69,6 +70,8 @@ public class TestPaymentInvoiceIntegration {
private PaymentApi paymentApi;
@Inject
private TestHelper testHelper;
+ @Inject
+ private NotificationQueueService notificationQueueService;
private MockPaymentInfoReceiver paymentInfoReceiver;
diff --git a/payment/src/test/java/com/ning/billing/payment/TestRetryService.java b/payment/src/test/java/com/ning/billing/payment/TestRetryService.java
new file mode 100644
index 0000000..6a08eb5
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/TestRetryService.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2010-2011 Ning, Inc.
+ *
+ * Ning 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 com.ning.billing.payment;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import com.google.inject.Inject;
+import com.ning.billing.account.api.Account;
+import com.ning.billing.account.glue.AccountModuleWithMocks;
+import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.invoice.api.Invoice;
+import com.ning.billing.invoice.glue.InvoiceModuleWithMocks;
+import com.ning.billing.invoice.model.RecurringInvoiceItem;
+import com.ning.billing.payment.api.Either;
+import com.ning.billing.payment.api.PaymentApi;
+import com.ning.billing.payment.api.PaymentAttempt;
+import com.ning.billing.payment.api.PaymentError;
+import com.ning.billing.payment.api.PaymentInfo;
+import com.ning.billing.payment.api.PaymentStatus;
+import com.ning.billing.payment.dao.PaymentDao;
+import com.ning.billing.payment.provider.MockPaymentProviderPlugin;
+import com.ning.billing.payment.provider.PaymentProviderPluginRegistry;
+import com.ning.billing.payment.setup.PaymentConfig;
+import com.ning.billing.payment.setup.PaymentTestModuleWithMocks;
+import com.ning.billing.util.bus.Bus;
+import com.ning.billing.util.notificationq.MockNotificationQueue;
+import com.ning.billing.util.notificationq.Notification;
+import com.ning.billing.util.notificationq.NotificationQueueService;
+
+@Guice(modules = { PaymentTestModuleWithMocks.class, AccountModuleWithMocks.class, InvoiceModuleWithMocks.class })
+@Test(groups = "fast")
+public class TestRetryService {
+ @Inject
+ private PaymentConfig paymentConfig;
+ @Inject
+ private Bus eventBus;
+ @Inject
+ private PaymentApi paymentApi;
+ @Inject
+ private TestHelper testHelper;
+ @Inject
+ private PaymentProviderPluginRegistry registry;
+ @Inject
+ private PaymentDao paymentDao;
+ @Inject
+ private RetryService retryService;
+ @Inject
+ private NotificationQueueService notificationQueueService;
+
+ private MockPaymentProviderPlugin mockPaymentProviderPlugin;
+ private MockNotificationQueue mockNotificationQueue;
+
+ @BeforeClass(alwaysRun = true)
+ public void initialize() throws Exception {
+ retryService.initialize();
+ }
+
+ @BeforeMethod(alwaysRun = true)
+ public void setUp() throws Exception {
+ eventBus.start();
+ retryService.start();
+
+ mockPaymentProviderPlugin = (MockPaymentProviderPlugin)registry.getPlugin(null);
+ mockNotificationQueue = (MockNotificationQueue)notificationQueueService.getNotificationQueue(RetryService.SERVICE_NAME, RetryService.QUEUE_NAME);
+ }
+
+ @AfterMethod(alwaysRun = true)
+ public void tearDown() throws Exception {
+ retryService.stop();
+ eventBus.stop();
+ }
+
+ @Test
+ public void testSchedulesRetry() throws Exception {
+ final DateTime now = new DateTime(DateTimeZone.UTC);
+ final Account account = testHelper.createTestCreditCardAccount();
+ final Invoice invoice = testHelper.createTestInvoice(account, now, Currency.USD);
+ final BigDecimal amount = new BigDecimal("10.00");
+ final UUID subscriptionId = UUID.randomUUID();
+
+ invoice.addInvoiceItem(new RecurringInvoiceItem(invoice.getId(),
+ subscriptionId,
+ "test plan", "test phase",
+ now,
+ now.plusMonths(1),
+ amount,
+ new BigDecimal("1.0"),
+ Currency.USD,
+ new DateTime(DateTimeZone.UTC),
+ new DateTime(DateTimeZone.UTC)));
+
+ mockPaymentProviderPlugin.makeNextInvoiceFail();
+
+ List<Either<PaymentError, PaymentInfo>> results = paymentApi.createPayment(account.getExternalKey(), Arrays.asList(invoice.getId().toString()));
+
+ assertEquals(results.size(), 1);
+ assertTrue(results.get(0).isLeft());
+
+ List<Notification> pendingNotifications = mockNotificationQueue.getPendingEvents();
+
+ assertEquals(pendingNotifications.size(), 1);
+
+ Notification notification = pendingNotifications.get(0);
+ PaymentAttempt paymentAttempt = paymentApi.getPaymentAttemptForInvoiceId(invoice.getId().toString());
+
+ assertNotNull(paymentAttempt);
+ assertEquals(notification.getNotificationKey(), paymentAttempt.getPaymentAttemptId().toString());
+ assertEquals(paymentAttempt.getRetryCount(), new Integer(1));
+
+ DateTime expectedRetryDate = paymentAttempt.getPaymentAttemptDate().plusDays(paymentConfig.getPaymentRetryDays().get(0));
+
+ assertEquals(notification.getEffectiveDate(), expectedRetryDate);
+ assertEquals(paymentAttempt.getNextRetryDate(), expectedRetryDate);
+ }
+
+ @Test
+ public void testRetries() throws Exception {
+ final DateTime now = new DateTime(DateTimeZone.UTC);
+ final Account account = testHelper.createTestCreditCardAccount();
+ final Invoice invoice = testHelper.createTestInvoice(account, now, Currency.USD);
+ final BigDecimal amount = new BigDecimal("10.00");
+ final UUID subscriptionId = UUID.randomUUID();
+
+ invoice.addInvoiceItem(new RecurringInvoiceItem(invoice.getId(),
+ subscriptionId,
+ "test plan", "test phase",
+ now,
+ now.plusMonths(1),
+ amount,
+ new BigDecimal("1.0"),
+ Currency.USD,
+ new DateTime(DateTimeZone.UTC),
+ new DateTime(DateTimeZone.UTC)));
+
+ DateTime nextRetryDate = new DateTime(DateTimeZone.UTC).minusDays(1);
+ DateTime paymentAttemptDate = nextRetryDate.minusDays(paymentConfig.getPaymentRetryDays().get(0));
+ PaymentAttempt paymentAttempt = new PaymentAttempt(UUID.randomUUID(), invoice).cloner()
+ .setRetryCount(1)
+ .setPaymentAttemptDate(paymentAttemptDate)
+ .setNextRetryDate(nextRetryDate)
+ .build();
+
+ paymentDao.createPaymentAttempt(paymentAttempt);
+ retryService.scheduleRetry(paymentAttempt, nextRetryDate);
+
+ // wait a little to give the queue time to process
+ Thread.sleep(paymentConfig.getNotificationSleepTimeMs() * 2);
+
+ List<Notification> pendingNotifications = mockNotificationQueue.getPendingEvents();
+
+ assertEquals(pendingNotifications.size(), 0);
+
+ List<PaymentInfo> paymentInfos = paymentApi.getPaymentInfo(Arrays.asList(invoice.getId().toString()));
+
+ assertEquals(paymentInfos.size(), 1);
+
+ PaymentInfo paymentInfo = paymentInfos.get(0);
+
+ assertEquals(paymentInfo.getStatus(), PaymentStatus.Processed.toString());
+
+ PaymentAttempt updatedAttempt = paymentApi.getPaymentAttemptForInvoiceId(invoice.getId().toString());
+
+ assertEquals(updatedAttempt.getPaymentAttemptId(), paymentAttempt.getPaymentAttemptId());
+ assertEquals(paymentInfo.getPaymentId(), updatedAttempt.getPaymentId());
+
+ }
+}
diff --git a/util/src/main/java/com/ning/billing/util/notificationq/DefaultNotificationQueue.java b/util/src/main/java/com/ning/billing/util/notificationq/DefaultNotificationQueue.java
index 8e2aaf8..c0c88fe 100644
--- a/util/src/main/java/com/ning/billing/util/notificationq/DefaultNotificationQueue.java
+++ b/util/src/main/java/com/ning/billing/util/notificationq/DefaultNotificationQueue.java
@@ -19,6 +19,7 @@ package com.ning.billing.util.notificationq;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+
import org.joda.time.DateTime;
import org.skife.jdbi.v2.IDBI;
import org.skife.jdbi.v2.sqlobject.mixins.Transmogrifier;
@@ -64,6 +65,12 @@ public class DefaultNotificationQueue extends NotificationQueueBase {
}
@Override
+ public void recordFutureNotification(DateTime futureNotificationTime, NotificationKey notificationKey) {
+ Notification notification = new DefaultNotification(getFullQName(), notificationKey.toString(), futureNotificationTime);
+ dao.insertNotification(notification);
+ }
+
+ @Override
public void recordFutureNotificationFromTransaction(final Transmogrifier transactionalDao,
final DateTime futureNotificationTime, final NotificationKey notificationKey) {
NotificationSqlDao transactionalNotificationDao = transactionalDao.become(NotificationSqlDao.class);
diff --git a/util/src/main/java/com/ning/billing/util/notificationq/NotificationQueue.java b/util/src/main/java/com/ning/billing/util/notificationq/NotificationQueue.java
index e1dcdbf..fb88d4c 100644
--- a/util/src/main/java/com/ning/billing/util/notificationq/NotificationQueue.java
+++ b/util/src/main/java/com/ning/billing/util/notificationq/NotificationQueue.java
@@ -23,9 +23,18 @@ import com.ning.billing.util.notificationq.NotificationQueueService.Notification
public interface NotificationQueue {
- /**
+ /**
+ *
+ * Record the need to be called back when the notification is ready
+ *
+ * @param futureNotificationTime the time at which the notification is ready
+ * @param notificationKey the key for that notification
+ */
+ public void recordFutureNotification(final DateTime futureNotificationTime, final NotificationKey notificationKey);
+
+ /**
*
- * Record from within a transaction the need to be called back when the notification is ready
+ * Record from within a transaction the need to be called back when the notification is ready
*
* @param transactionalDao the transactionalDao
* @param futureNotificationTime the time at which the notification is ready
diff --git a/util/src/test/java/com/ning/billing/util/notificationq/MockNotificationQueue.java b/util/src/test/java/com/ning/billing/util/notificationq/MockNotificationQueue.java
index e96d2cf..b76a8ad 100644
--- a/util/src/test/java/com/ning/billing/util/notificationq/MockNotificationQueue.java
+++ b/util/src/test/java/com/ning/billing/util/notificationq/MockNotificationQueue.java
@@ -49,9 +49,7 @@ public class MockNotificationQueue extends NotificationQueueBase implements Noti
}
@Override
- public void recordFutureNotificationFromTransaction(
- Transmogrifier transactionalDao, DateTime futureNotificationTime,
- NotificationKey notificationKey) {
+ public void recordFutureNotification(DateTime futureNotificationTime, NotificationKey notificationKey) {
Notification notification = new DefaultNotification("MockQueue", notificationKey.toString(), futureNotificationTime);
synchronized(notifications) {
notifications.add(notification);
@@ -59,6 +57,24 @@ public class MockNotificationQueue extends NotificationQueueBase implements Noti
}
@Override
+ public void recordFutureNotificationFromTransaction(
+ Transmogrifier transactionalDao, DateTime futureNotificationTime,
+ NotificationKey notificationKey) {
+ recordFutureNotification(futureNotificationTime, notificationKey);
+ }
+
+ public List<Notification> getPendingEvents() {
+ List<Notification> result = new ArrayList<Notification>();
+
+ for (Notification notification : notifications) {
+ if (notification.getProcessingState() == NotificationLifecycleState.AVAILABLE) {
+ result.add(notification);
+ }
+ }
+ return result;
+ }
+
+ @Override
protected int doProcessEvents(int sequenceId) {
int result = 0;