killbill-memoizeit
Changes
analytics/pom.xml 1(+0 -1)
analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java 275(+166 -109)
analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java 3(+3 -0)
analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg 8(+7 -1)
analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java 5(+5 -0)
Details
analytics/pom.xml 1(+0 -1)
diff --git a/analytics/pom.xml b/analytics/pom.xml
index ecd27bf..c95818d 100644
--- a/analytics/pom.xml
+++ b/analytics/pom.xml
@@ -117,7 +117,6 @@
<dependency>
<groupId>com.ning.billing</groupId>
<artifactId>killbill-util</artifactId>
- <scope>test</scope>
</dependency>
<dependency>
<groupId>com.ning.billing</groupId>
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 247a438..bcddd64 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/AnalyticsListener.java
@@ -25,7 +25,6 @@ import com.ning.billing.entitlement.api.timeline.RepairEntitlementEvent;
import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.RequestedSubscriptionEvent;
-import com.ning.billing.entitlement.api.user.SubscriptionEvent;
import com.ning.billing.invoice.api.EmptyInvoiceEvent;
import com.ning.billing.invoice.api.InvoiceCreationEvent;
import com.ning.billing.payment.api.PaymentErrorEvent;
@@ -58,14 +57,18 @@ public class AnalyticsListener {
@Subscribe
public void handleEffectiveSubscriptionTransitionChange(final EffectiveSubscriptionEvent eventEffective) throws AccountApiException, EntitlementUserApiException {
- handleSubscriptionTransitionChange(eventEffective);
+ bstRecorder.rebuildTransitionsForBundle(eventEffective.getBundleId());
}
@Subscribe
public void handleRequestedSubscriptionTransitionChange(final RequestedSubscriptionEvent eventRequested) throws AccountApiException, EntitlementUserApiException {
- if (eventRequested.getEffectiveTransitionTime().isAfter(eventRequested.getRequestedTransitionTime())) {
- handleSubscriptionTransitionChange(eventRequested);
- }
+ bstRecorder.rebuildTransitionsForBundle(eventRequested.getBundleId());
+ }
+
+ @Subscribe
+ public void handleRepairEntitlement(final RepairEntitlementEvent event) {
+ // In case of repair, just rebuild all transitions
+ bstRecorder.rebuildTransitionsForBundle(event.getBundleId());
}
@Subscribe
@@ -84,6 +87,7 @@ public class AnalyticsListener {
@Subscribe
public void handleInvoiceCreation(final InvoiceCreationEvent event) {
+ // TODO - follow same logic as entitlements to support repair
invoiceRecorder.invoiceCreated(event.getInvoiceId());
}
@@ -141,37 +145,4 @@ public class AnalyticsListener {
public void handleUserTagDefinitionDeletion(final UserTagDefinitionDeletionEvent event) {
// Ignored for now
}
-
- @Subscribe
- public void handleRepairEntitlement(final RepairEntitlementEvent event) {
- // Ignored for now
- }
-
- private void handleSubscriptionTransitionChange(final SubscriptionEvent eventEffective) throws AccountApiException, EntitlementUserApiException {
- switch (eventEffective.getTransitionType()) {
- // A subscription enters either through migration or as newly created subscription
- case MIGRATE_ENTITLEMENT:
- case CREATE:
- bstRecorder.subscriptionCreated(eventEffective);
- break;
- case RE_CREATE:
- bstRecorder.subscriptionRecreated(eventEffective);
- break;
- case MIGRATE_BILLING:
- break;
- case CANCEL:
- bstRecorder.subscriptionCancelled(eventEffective);
- break;
- case CHANGE:
- bstRecorder.subscriptionChanged(eventEffective);
- break;
- case UNCANCEL:
- break;
- case PHASE:
- bstRecorder.subscriptionPhaseChanged(eventEffective);
- break;
- default:
- throw new RuntimeException("Unexpected event type " + eventEffective.getTransitionType());
- }
- }
}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
index e0fbab0..a4c9ff3 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/BusinessSubscriptionTransitionRecorder.java
@@ -16,7 +16,9 @@
package com.ning.billing.analytics;
+import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
import org.joda.time.DateTime;
import org.skife.jdbi.v2.Transaction;
@@ -34,10 +36,14 @@ import com.ning.billing.analytics.model.BusinessSubscriptionEvent;
import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
import com.ning.billing.catalog.api.CatalogService;
import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.entitlement.api.SubscriptionTransitionType;
+import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.EntitlementUserApiException;
+import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.entitlement.api.user.SubscriptionEvent;
+import com.ning.billing.util.clock.Clock;
public class BusinessSubscriptionTransitionRecorder {
private static final Logger log = LoggerFactory.getLogger(BusinessSubscriptionTransitionRecorder.class);
@@ -46,90 +52,194 @@ public class BusinessSubscriptionTransitionRecorder {
private final EntitlementUserApi entitlementApi;
private final AccountUserApi accountApi;
private final CatalogService catalogService;
+ private final Clock clock;
@Inject
- public BusinessSubscriptionTransitionRecorder(final BusinessSubscriptionTransitionSqlDao sqlDao, final CatalogService catalogService, final EntitlementUserApi entitlementApi, final AccountUserApi accountApi) {
+ public BusinessSubscriptionTransitionRecorder(final BusinessSubscriptionTransitionSqlDao sqlDao,
+ final CatalogService catalogService,
+ final EntitlementUserApi entitlementApi,
+ final AccountUserApi accountApi,
+ final Clock clock) {
this.sqlDao = sqlDao;
this.catalogService = catalogService;
this.entitlementApi = entitlementApi;
this.accountApi = accountApi;
+ this.clock = clock;
}
- public void subscriptionCreated(final SubscriptionEvent created) throws AccountApiException, EntitlementUserApiException {
- final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCreated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
- recordTransition(event, created);
+ public void rebuildTransitionsForBundle(final UUID bundleId) {
+ final SubscriptionBundle bundle;
+ try {
+ bundle = entitlementApi.getBundleFromId(bundleId);
+ } catch (EntitlementUserApiException e) {
+ log.warn("Ignoring update for bundle {}: bundle does not exist", bundleId);
+ return;
+ }
+
+ final Account account;
+ try {
+ account = accountApi.getAccountById(bundle.getAccountId());
+ } catch (AccountApiException e) {
+ log.warn("Ignoring update for bundle {}: account {} does not exist", bundleId, bundle.getAccountId());
+ return;
+ }
+
+ final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundleId);
+
+ final String externalKey = bundle.getKey();
+ final String accountKey = account.getExternalKey();
+ final Currency currency = account.getCurrency();
+
+ sqlDao.inTransaction(new Transaction<Void, BusinessSubscriptionTransitionSqlDao>() {
+ @Override
+ public Void inTransaction(final BusinessSubscriptionTransitionSqlDao transactional, final TransactionStatus status) throws Exception {
+ log.info("Started rebuilding transitions for bundle {}", externalKey);
+ transactional.deleteTransitionsForBundle(externalKey);
+
+ final ArrayList<BusinessSubscriptionTransition> transitions = new ArrayList<BusinessSubscriptionTransition>();
+ for (final Subscription subscription : subscriptions) {
+ for (final EffectiveSubscriptionEvent event : subscription.getAllTransitions()) {
+ final BusinessSubscriptionEvent businessEvent = getBusinessSubscriptionFromEvent(event);
+ if (businessEvent == null) {
+ continue;
+ }
+
+ final BusinessSubscription prevSubscription = createPreviousBusinessSubscription(event, businessEvent, transitions, currency);
+ final BusinessSubscription nextSubscription = createNextBusinessSubscription(event, businessEvent, currency);
+ final BusinessSubscriptionTransition transition = new BusinessSubscriptionTransition(
+ event.getTotalOrdering(),
+ externalKey,
+ accountKey,
+ event.getRequestedTransitionTime(),
+ businessEvent,
+ prevSubscription,
+ nextSubscription
+ );
+
+ transactional.createTransition(transition);
+ transitions.add(transition);
+ log.info("Adding transition {}", transition);
+
+ // We need to manually add the system cancel event
+ if (SubscriptionTransitionType.CANCEL.equals(event.getTransitionType()) &&
+ clock.getUTCNow().isAfter(event.getEffectiveTransitionTime())) {
+ final BusinessSubscriptionTransition systemCancelTransition = new BusinessSubscriptionTransition(
+ event.getTotalOrdering(),
+ externalKey,
+ accountKey,
+ event.getRequestedTransitionTime(),
+ new BusinessSubscriptionEvent(BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL, businessEvent.getCategory()),
+ prevSubscription,
+ nextSubscription
+ );
+ transactional.createTransition(systemCancelTransition);
+ transitions.add(systemCancelTransition);
+ log.info("Adding transition {}", systemCancelTransition);
+ }
+ }
+ }
+
+ log.info("Finished rebuilding transitions for bundle {}", externalKey);
+ return null;
+ }
+ });
+ }
+
+ private BusinessSubscriptionEvent getBusinessSubscriptionFromEvent(final SubscriptionEvent event) throws AccountApiException, EntitlementUserApiException {
+ switch (event.getTransitionType()) {
+ // A subscription enters either through migration or as newly created subscription
+ case MIGRATE_ENTITLEMENT:
+ case CREATE:
+ return subscriptionCreated(event);
+ case RE_CREATE:
+ return subscriptionRecreated(event);
+ case CANCEL:
+ return subscriptionCancelled(event);
+ case CHANGE:
+ return subscriptionChanged(event);
+ case PHASE:
+ return subscriptionPhaseChanged(event);
+ // TODO - should we really ignore these?
+ case MIGRATE_BILLING:
+ case UNCANCEL:
+ return null;
+ default:
+ log.warn("Unexpected event type " + event.getTransitionType());
+ return null;
+ }
+ }
+
+ private BusinessSubscriptionEvent subscriptionCreated(final SubscriptionEvent created) throws AccountApiException, EntitlementUserApiException {
+ return BusinessSubscriptionEvent.subscriptionCreated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
}
- public void subscriptionRecreated(final SubscriptionEvent recreated) throws AccountApiException, EntitlementUserApiException {
- final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
- recordTransition(event, recreated);
+ private BusinessSubscriptionEvent subscriptionRecreated(final SubscriptionEvent recreated) throws AccountApiException, EntitlementUserApiException {
+ return BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
}
- public void subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
+ private BusinessSubscriptionEvent subscriptionCancelled(final SubscriptionEvent cancelled) throws AccountApiException, EntitlementUserApiException {
// cancelled.getNextPlan() is null here - need to look at the previous one to create the correct event name
- final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
- recordTransition(event, cancelled);
+ return BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
}
- public void subscriptionChanged(final SubscriptionEvent changed) throws AccountApiException, EntitlementUserApiException {
- final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionChanged(changed.getNextPlan(), catalogService.getFullCatalog(), changed.getEffectiveTransitionTime(), changed.getSubscriptionStartDate());
- recordTransition(event, changed);
+ private BusinessSubscriptionEvent subscriptionChanged(final SubscriptionEvent changed) throws AccountApiException, EntitlementUserApiException {
+ return BusinessSubscriptionEvent.subscriptionChanged(changed.getNextPlan(), catalogService.getFullCatalog(), changed.getEffectiveTransitionTime(), changed.getSubscriptionStartDate());
}
- public void subscriptionPhaseChanged(final SubscriptionEvent phaseChanged) throws AccountApiException, EntitlementUserApiException {
- final BusinessSubscriptionEvent event = BusinessSubscriptionEvent.subscriptionPhaseChanged(phaseChanged.getNextPlan(), phaseChanged.getNextState(), catalogService.getFullCatalog(), phaseChanged.getEffectiveTransitionTime(), phaseChanged.getSubscriptionStartDate());
- recordTransition(event, phaseChanged);
+ private BusinessSubscriptionEvent subscriptionPhaseChanged(final SubscriptionEvent phaseChanged) throws AccountApiException, EntitlementUserApiException {
+ return BusinessSubscriptionEvent.subscriptionPhaseChanged(phaseChanged.getNextPlan(), phaseChanged.getNextState(), catalogService.getFullCatalog(), phaseChanged.getEffectiveTransitionTime(), phaseChanged.getSubscriptionStartDate());
}
- void recordTransition(final BusinessSubscriptionEvent event, final SubscriptionEvent transition) throws AccountApiException, EntitlementUserApiException {
- Currency currency = null;
- String externalKey = null;
- String accountKey = null;
+ private BusinessSubscription createNextBusinessSubscription(final EffectiveSubscriptionEvent event, final BusinessSubscriptionEvent businessEvent, final Currency currency) {
+ final BusinessSubscription nextSubscription;
+ if (BusinessSubscriptionEvent.EventType.CANCEL.equals(businessEvent.getEventType()) ||
+ BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL.equals(businessEvent.getEventType())) {
+ nextSubscription = null;
+ } else {
+ nextSubscription = new BusinessSubscription(event.getNextPriceList(), event.getNextPlan(), event.getNextPhase(),
+ currency, event.getEffectiveTransitionTime(), event.getNextState(),
+ event.getSubscriptionId(), event.getBundleId(), catalogService.getFullCatalog());
+ }
- // Retrieve key and currency via the bundle
- final SubscriptionBundle bundle = entitlementApi.getBundleFromId(transition.getBundleId());
- if (bundle != null) {
- externalKey = bundle.getKey();
+ return nextSubscription;
+ }
- final Account account = accountApi.getAccountById(bundle.getAccountId());
- if (account != null) {
- accountKey = account.getExternalKey();
- currency = account.getCurrency();
- }
+ private BusinessSubscription createPreviousBusinessSubscription(final EffectiveSubscriptionEvent event,
+ final BusinessSubscriptionEvent businessEvent,
+ final ArrayList<BusinessSubscriptionTransition> transitions,
+ final Currency currency) {
+ if (BusinessSubscriptionEvent.EventType.ADD.equals(businessEvent.getEventType()) ||
+ BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType())) {
+ return null;
}
- // The SubscriptionEvent interface gives us all the prev/next information we need but the start date
- // of the previous phase. We need to retrieve it from our own transitions table
- DateTime previousEffectiveTransitionTime = null;
- // For (re-)creation events, the prev subscription will always be null
- if (!BusinessSubscriptionEvent.EventType.ADD.equals(event.getEventType()) && !BusinessSubscriptionEvent.EventType.RE_ADD.equals(event.getEventType())) {
- final List<BusinessSubscriptionTransition> transitions = sqlDao.getTransitions(externalKey);
- if (transitions != null && transitions.size() > 0) {
- for (final BusinessSubscriptionTransition candidate : transitions) {
- if (candidate != null && candidate.getNextSubscription() != null && candidate.getNextSubscription().getStartDate().isBefore(transition.getEffectiveTransitionTime())) {
- previousEffectiveTransitionTime = candidate.getNextSubscription().getStartDate();
- }
- }
+ final BusinessSubscriptionTransition prevTransition = getPreviousBusinessSubscriptionTransitionForEvent(event, transitions);
+ return new BusinessSubscription(event.getPreviousPriceList(), event.getPreviousPlan(), event.getPreviousPhase(),
+ currency, prevTransition.getNextSubscription().getStartDate(), event.getPreviousState(),
+ event.getSubscriptionId(), event.getBundleId(), catalogService.getFullCatalog());
+ }
+
+ private BusinessSubscriptionTransition getPreviousBusinessSubscriptionTransitionForEvent(final EffectiveSubscriptionEvent event,
+ final ArrayList<BusinessSubscriptionTransition> transitions) {
+ BusinessSubscriptionTransition transition = null;
+ for (final BusinessSubscriptionTransition candidate : transitions) {
+ final BusinessSubscription nextSubscription = candidate.getNextSubscription();
+ if (nextSubscription == null || !nextSubscription.getStartDate().isBefore(event.getEffectiveTransitionTime())) {
+ continue;
}
- }
- // TODO Support currency changes
- final BusinessSubscription prevSubscription;
- if (previousEffectiveTransitionTime == null) {
- prevSubscription = null;
- } else {
- prevSubscription = new BusinessSubscription(transition.getPreviousPriceList(), transition.getPreviousPlan(), transition.getPreviousPhase(), currency, previousEffectiveTransitionTime, transition.getPreviousState(), transition.getSubscriptionId(), transition.getBundleId(), catalogService.getFullCatalog());
+ if (nextSubscription.getSubscriptionId().equals(event.getSubscriptionId())) {
+ transition = candidate;
+ }
}
- final BusinessSubscription nextSubscription;
- // next plan is null for CANCEL events
- if (transition.getNextPlan() == null) {
- nextSubscription = null;
- } else {
- nextSubscription = new BusinessSubscription(transition.getNextPriceList(), transition.getNextPlan(), transition.getNextPhase(), currency, transition.getEffectiveTransitionTime(), transition.getNextState(), transition.getSubscriptionId(), transition.getBundleId(), catalogService.getFullCatalog());
+ if (transition == null) {
+ log.error("Unable to retrieve the previous transition - THIS SHOULD NEVER HAPPEN");
+ // Fall back to the latest one?
+ transition = transitions.get(transitions.size() - 1);
}
- record(transition.getTotalOrdering(), externalKey, accountKey, transition.getRequestedTransitionTime(), event, prevSubscription, nextSubscription);
+ return transition;
}
// Public for internal reasons
@@ -143,59 +253,6 @@ public class BusinessSubscriptionTransitionRecorder {
prevSubscription,
nextSubscription
);
-
- log.info(transition.getEvent() + " " + transition);
- sqlDao.inTransaction(new Transaction<Void, BusinessSubscriptionTransitionSqlDao>() {
- @Override
- public Void inTransaction(final BusinessSubscriptionTransitionSqlDao transactional, final TransactionStatus status) throws Exception {
- final String subscriptionId;
- if (nextSubscription != null && nextSubscription.getSubscriptionId() != null) {
- subscriptionId = nextSubscription.getSubscriptionId().toString();
- } else {
- subscriptionId = prevSubscription.getSubscriptionId().toString();
- }
-
- // There is no ordering guaranteed with events on the bus. This can be problematic on e.g. subscription creation:
- // the requested future change from trial to evergreen could be received before the actual creation event.
- // In this case, we would have two subscriptions in BST, with both null for the previous transition.
- // To work around this, we need to update bst as we go
- if (BusinessSubscriptionEvent.EventType.ADD.equals(event.getEventType())) {
- final List<BusinessSubscriptionTransition> transitions = transactional.getTransitionForSubscription(subscriptionId);
- if (transitions != null && transitions.size() > 0) {
- final BusinessSubscriptionTransition firstTransition = transitions.get(0);
- // Ignore (re-)creation events here, the previous subscription is expected to be null
- if (!BusinessSubscriptionEvent.EventType.ADD.equals(firstTransition.getEvent().getEventType()) &&
- !BusinessSubscriptionEvent.EventType.RE_ADD.equals(firstTransition.getEvent().getEventType()) &&
- firstTransition.getPreviousSubscription() == null) {
- final BusinessSubscriptionTransition updatedFirstTransition = new BusinessSubscriptionTransition(
- firstTransition.getTotalOrdering(),
- firstTransition.getExternalKey(),
- firstTransition.getAccountKey(),
- firstTransition.getRequestedTimestamp(),
- firstTransition.getEvent(),
- nextSubscription,
- firstTransition.getNextSubscription()
- );
- transactional.updateTransition(updatedFirstTransition.getTotalOrdering(), updatedFirstTransition);
- }
- }
- }
-
- // Ignore duplicates: for e.g. phase events, we may already have recorded the transition when the change
- // was requested. In that case, ignore it
- final List<BusinessSubscriptionTransition> currentTransitions = transactional.getTransitionForSubscription(subscriptionId);
- if (currentTransitions != null && currentTransitions.size() > 0) {
- for (final BusinessSubscriptionTransition currentTransition : currentTransitions) {
- if (currentTransition.isDuplicateOf(transition)) {
- return null;
- }
- }
- }
-
- transactional.createTransition(transition);
-
- return null;
- }
- });
+ sqlDao.createTransition(transition);
}
}
diff --git a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
index 5b2838c..2092d47 100644
--- a/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/main/java/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.java
@@ -43,5 +43,8 @@ public interface BusinessSubscriptionTransitionSqlDao extends Transactional<Busi
void updateTransition(@Bind("total_ordering") long totalOrdering, @BusinessSubscriptionTransitionBinder BusinessSubscriptionTransition updatedFirstTransition);
@SqlUpdate
+ void deleteTransitionsForBundle(@Bind("external_key") final String externalKey);
+
+ @SqlUpdate
void test();
}
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
index 2959682..7192e5f 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
+++ b/analytics/src/main/resources/com/ning/billing/analytics/dao/BusinessSubscriptionTransitionSqlDao.sql.stg
@@ -37,7 +37,7 @@ getTransitions(external_key) ::= <<
, next_bundle_id
from bst
where external_key=:external_key
- order by prev_start_date, next_start_date, requested_timestamp asc
+ order by requested_timestamp asc
;
>>
@@ -192,6 +192,12 @@ updateTransition() ::= <<
where total_ordering = :total_ordering
>>
+deleteTransitionsForBundle(external_key) ::= <<
+ delete from bst
+ where external_key=:external_key
+ ;
+>>
+
test() ::= <<
select 1 from bst;
>>
diff --git a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
index 43aaad8..366570c 100644
--- a/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
+++ b/analytics/src/main/resources/com/ning/billing/analytics/ddl.sql
@@ -1,6 +1,7 @@
drop table if exists bst;
create table bst (
- total_ordering bigint default 0
+ record_id int(11) unsigned not null auto_increment
+, total_ordering bigint default 0
, external_key varchar(50) not null comment 'Bundle external key'
, account_key varchar(50) not null comment 'Account external key'
, requested_timestamp bigint not null
@@ -33,7 +34,7 @@ create table bst (
, next_state varchar(32) default null
, next_subscription_id varchar(100) default null
, next_bundle_id varchar(100) default null
-, primary key(total_ordering)
+, primary key(record_id)
) engine=innodb comment 'Business Subscription Transitions, track bundles lifecycle';
create index bst_key_index on bst (external_key, requested_timestamp asc);
diff --git a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
index 8083833..b54d7ce 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/MockBusinessSubscriptionTransitionSqlDao.java
@@ -58,6 +58,11 @@ public class MockBusinessSubscriptionTransitionSqlDao implements BusinessSubscri
}
@Override
+ public void deleteTransitionsForBundle(@Bind("external_key") final String externalKey) {
+ content.put(externalKey, new ArrayList<BusinessSubscriptionTransition>());
+ }
+
+ @Override
public void test() {
}
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
index 9dbc209..48cefb4 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessSubscriptionTransitionRecorder.java
@@ -24,23 +24,24 @@ import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.Test;
+import com.google.common.collect.ImmutableList;
import com.ning.billing.account.api.Account;
import com.ning.billing.account.api.AccountUserApi;
import com.ning.billing.analytics.dao.BusinessSubscriptionTransitionSqlDao;
-import com.ning.billing.analytics.model.BusinessSubscription;
-import com.ning.billing.analytics.model.BusinessSubscriptionEvent;
import com.ning.billing.analytics.model.BusinessSubscriptionTransition;
import com.ning.billing.catalog.api.Catalog;
import com.ning.billing.catalog.api.CatalogService;
-import com.ning.billing.catalog.api.Currency;
+import com.ning.billing.entitlement.api.SubscriptionTransitionType;
import com.ning.billing.entitlement.api.user.EffectiveSubscriptionEvent;
import com.ning.billing.entitlement.api.user.EntitlementUserApi;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
+import com.ning.billing.util.clock.DefaultClock;
public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSuite {
@Test(groups = "fast")
public void testCreateAddOn() throws Exception {
+ final UUID bundleId = UUID.randomUUID();
final UUID externalKey = UUID.randomUUID();
// Setup the catalog
@@ -49,26 +50,10 @@ public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSui
// Setup the dao
final BusinessSubscriptionTransitionSqlDao sqlDao = new MockBusinessSubscriptionTransitionSqlDao();
- // Add a previous subscription to make sure it doesn't impact the addon
- final BusinessSubscription nextPrevSubscription = new BusinessSubscription(UUID.randomUUID().toString(),
- UUID.randomUUID().toString(),
- UUID.randomUUID().toString(),
- Currency.USD,
- new DateTime(DateTimeZone.UTC),
- Subscription.SubscriptionState.ACTIVE,
- UUID.randomUUID(),
- UUID.randomUUID(),
- catalogService.getFullCatalog());
- sqlDao.createTransition(new BusinessSubscriptionTransition(10L,
- externalKey.toString(),
- UUID.randomUUID().toString(),
- new DateTime(DateTimeZone.UTC),
- BusinessSubscriptionEvent.valueOf("ADD_MISC"),
- null,
- nextPrevSubscription));
// Setup the entitlement API
final SubscriptionBundle bundle = Mockito.mock(SubscriptionBundle.class);
+ Mockito.when(bundle.getId()).thenReturn(bundleId);
Mockito.when(bundle.getKey()).thenReturn(externalKey.toString());
final EntitlementUserApi entitlementApi = Mockito.mock(EntitlementUserApi.class);
Mockito.when(entitlementApi.getBundleFromId(Mockito.<UUID>any())).thenReturn(bundle);
@@ -79,20 +64,25 @@ public class TestBusinessSubscriptionTransitionRecorder extends AnalyticsTestSui
final AccountUserApi accountApi = Mockito.mock(AccountUserApi.class);
Mockito.when(accountApi.getAccountById(bundle.getAccountId())).thenReturn(account);
- final BusinessSubscriptionTransitionRecorder recorder = new BusinessSubscriptionTransitionRecorder(sqlDao, catalogService, entitlementApi, accountApi);
-
// Create an new subscription event
final EffectiveSubscriptionEvent eventEffective = Mockito.mock(EffectiveSubscriptionEvent.class);
Mockito.when(eventEffective.getId()).thenReturn(UUID.randomUUID());
+ Mockito.when(eventEffective.getTransitionType()).thenReturn(SubscriptionTransitionType.CREATE);
Mockito.when(eventEffective.getSubscriptionId()).thenReturn(UUID.randomUUID());
Mockito.when(eventEffective.getRequestedTransitionTime()).thenReturn(new DateTime(DateTimeZone.UTC));
Mockito.when(eventEffective.getNextPlan()).thenReturn(UUID.randomUUID().toString());
Mockito.when(eventEffective.getEffectiveTransitionTime()).thenReturn(new DateTime(DateTimeZone.UTC));
Mockito.when(eventEffective.getSubscriptionStartDate()).thenReturn(new DateTime(DateTimeZone.UTC));
- recorder.subscriptionCreated(eventEffective);
- Assert.assertEquals(sqlDao.getTransitions(externalKey.toString()).size(), 2);
- final BusinessSubscriptionTransition transition = sqlDao.getTransitions(externalKey.toString()).get(1);
+ final Subscription subscription = Mockito.mock(Subscription.class);
+ Mockito.when(subscription.getAllTransitions()).thenReturn(ImmutableList.<EffectiveSubscriptionEvent>of(eventEffective));
+ Mockito.when(entitlementApi.getSubscriptionsForBundle(Mockito.<UUID>any())).thenReturn(ImmutableList.<Subscription>of(subscription));
+
+ final BusinessSubscriptionTransitionRecorder recorder = new BusinessSubscriptionTransitionRecorder(sqlDao, catalogService, entitlementApi, accountApi, new DefaultClock());
+ recorder.rebuildTransitionsForBundle(bundle.getId());
+
+ Assert.assertEquals(sqlDao.getTransitions(externalKey.toString()).size(), 1);
+ final BusinessSubscriptionTransition transition = sqlDao.getTransitions(externalKey.toString()).get(0);
Assert.assertEquals(transition.getTotalOrdering(), (long) eventEffective.getTotalOrdering());
Assert.assertEquals(transition.getAccountKey(), externalKey.toString());
// Make sure all the prev_ columns are null
diff --git a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
index 6f18739..79e762f 100644
--- a/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
+++ b/analytics/src/test/java/com/ning/billing/analytics/TestBusinessTagRecorder.java
@@ -85,7 +85,7 @@ public class TestBusinessTagRecorder extends TestWithEmbeddedDB {
final CatalogService catalogService = new DefaultCatalogService(Mockito.mock(CatalogConfig.class), Mockito.mock(VersionedCatalogLoader.class));
final AddonUtils addonUtils = new AddonUtils(catalogService);
final DefaultNotificationQueueService notificationQueueService = new DefaultNotificationQueueService(dbi, clock);
- final EntitlementDao entitlementDao = new AuditedEntitlementDao(dbi, clock, addonUtils, notificationQueueService, eventBus);
+ final EntitlementDao entitlementDao = new AuditedEntitlementDao(dbi, clock, addonUtils, notificationQueueService, eventBus, catalogService);
final PlanAligner planAligner = new PlanAligner(catalogService);
final DefaultSubscriptionApiService apiService = new DefaultSubscriptionApiService(clock, entitlementDao, catalogService, planAligner);
final DefaultSubscriptionFactory subscriptionFactory = new DefaultSubscriptionFactory(apiService, clock, catalogService);
diff --git a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
index 8a72880..94421a4 100644
--- a/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
+++ b/beatrix/src/test/java/com/ning/billing/beatrix/integration/TestAnalytics.java
@@ -98,10 +98,11 @@ public class TestAnalytics extends TestIntegrationBase {
subscription.cancel(clock.getUTCNow(), true, context);
waitALittle();
+
verifyBSTWithTrialAndEvergreenPhasesAndCancellation(account, bundle, subscription);
// Move after cancel date
- clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS);
+ clock.addDeltaFromReality(AT_LEAST_ONE_MONTH_MS + 100);
assertTrue(busHandler.isCompleted(DELAY));
waitALittle();
@@ -296,7 +297,7 @@ public class TestAnalytics extends TestIntegrationBase {
Assert.assertEquals(transitions.size(), 3);
verifyTrialAndEvergreenPhases(account, bundle, subscription);
- verifyCancellationTransition(account, bundle, subscription);
+ verifyCancellationTransition(account, bundle);
}
private void verifyBSTWithTrialAndEvergreenPhasesAndCancellationAndSystemCancellation(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
@@ -305,8 +306,8 @@ public class TestAnalytics extends TestIntegrationBase {
Assert.assertEquals(transitions.size(), 4);
verifyTrialAndEvergreenPhases(account, bundle, subscription);
- verifyCancellationTransition(account, bundle, subscription);
- verifySystemCancellationTransition(account, bundle, subscription);
+ verifyCancellationTransition(account, bundle);
+ verifySystemCancellationTransition(account, bundle);
}
private void verifyTrialAndEvergreenPhases(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
@@ -365,7 +366,7 @@ public class TestAnalytics extends TestIntegrationBase {
Assert.assertEquals(futureTransition.getNextSubscription().getSubscriptionId(), subscription.getId());
}
- private void verifyCancellationTransition(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
+ private void verifyCancellationTransition(final Account account, final SubscriptionBundle bundle) throws CatalogApiException {
final Product currentProduct = subscriptionPlan.getProduct();
final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
@@ -380,16 +381,13 @@ public class TestAnalytics extends TestIntegrationBase {
Assert.assertEquals(cancellationRequest.getPreviousSubscription(), transitions.get(1).getNextSubscription());
}
- private void verifySystemCancellationTransition(final Account account, final SubscriptionBundle bundle, final Subscription subscription) throws CatalogApiException {
- final Plan currentPlan = subscription.getCurrentPlan();
- final Product currentProduct = currentPlan.getProduct();
-
+ private void verifySystemCancellationTransition(final Account account, final SubscriptionBundle bundle) throws CatalogApiException {
final List<BusinessSubscriptionTransition> transitions = analyticsUserApi.getTransitionsForBundle(bundle.getKey());
final BusinessSubscriptionTransition systemCancellation = transitions.get(3);
Assert.assertEquals(systemCancellation.getExternalKey(), bundle.getKey());
Assert.assertEquals(systemCancellation.getAccountKey(), account.getExternalKey());
- Assert.assertEquals(systemCancellation.getEvent().getCategory(), currentProduct.getCategory());
+ Assert.assertEquals(systemCancellation.getEvent().getCategory(), ProductCategory.BASE);
Assert.assertEquals(systemCancellation.getEvent().getEventType(), BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL);
Assert.assertNull(systemCancellation.getNextSubscription());