/*
* 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.analytics;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.skife.jdbi.v2.Transaction;
import org.skife.jdbi.v2.TransactionStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.billing.account.api.Account;
import com.ning.billing.account.api.AccountApiException;
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.BusinessSubscriptionTransitionModelDao;
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.EntitlementUserApiException;
import com.ning.billing.entitlement.api.user.Subscription;
import com.ning.billing.entitlement.api.user.SubscriptionBundle;
import com.ning.billing.util.callcontext.InternalCallContext;
import com.ning.billing.util.clock.Clock;
import com.ning.billing.util.events.EffectiveSubscriptionInternalEvent;
import com.ning.billing.util.events.SubscriptionInternalEvent;
import com.ning.billing.util.svcapi.account.AccountInternalApi;
import com.ning.billing.util.svcapi.entitlement.EntitlementInternalApi;
import com.google.inject.Inject;
public class BusinessSubscriptionTransitionDao {
private static final Logger log = LoggerFactory.getLogger(BusinessSubscriptionTransitionDao.class);
private final BusinessSubscriptionTransitionSqlDao sqlDao;
private final EntitlementInternalApi entitlementApi;
private final AccountInternalApi accountApi;
private final CatalogService catalogService;
private final Clock clock;
@Inject
public BusinessSubscriptionTransitionDao(final BusinessSubscriptionTransitionSqlDao sqlDao,
final CatalogService catalogService,
final EntitlementInternalApi entitlementApi,
final AccountInternalApi accountApi,
final Clock clock) {
this.sqlDao = sqlDao;
this.catalogService = catalogService;
this.entitlementApi = entitlementApi;
this.accountApi = accountApi;
this.clock = clock;
}
public void rebuildTransitionsForBundle(final UUID bundleId, final InternalCallContext context) {
final SubscriptionBundle bundle;
try {
bundle = entitlementApi.getBundleFromId(bundleId, context);
} catch (EntitlementUserApiException e) {
log.warn("Ignoring update for bundle {}: bundle does not exist", bundleId);
return;
}
final Account account;
try {
account = accountApi.getAccountById(bundle.getAccountId(), context);
} catch (AccountApiException e) {
log.warn("Ignoring update for bundle {}: account {} does not exist", bundleId, bundle.getAccountId());
return;
}
final List<Subscription> subscriptions = entitlementApi.getSubscriptionsForBundle(bundleId, context);
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 id {}", bundleId);
transactional.deleteTransitionsForBundle(bundleId.toString(), context);
final ArrayList<BusinessSubscriptionTransitionModelDao> transitions = new ArrayList<BusinessSubscriptionTransitionModelDao>();
for (final Subscription subscription : subscriptions) {
// TODO remove API call from within transaction, although this is NOT a real issue as this call wil not hit the DB
for (final EffectiveSubscriptionInternalEvent event : entitlementApi.getAllTransitions(subscription, context)) {
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 BusinessSubscriptionTransitionModelDao transition = new BusinessSubscriptionTransitionModelDao(
event.getTotalOrdering(),
bundleId,
bundle.getExternalKey(),
bundle.getAccountId(),
account.getExternalKey(),
subscription.getId(),
event.getRequestedTransitionTime(),
businessEvent,
prevSubscription,
nextSubscription
);
transactional.createTransition(transition, context);
transitions.add(transition);
log.info("Adding transition {}", transition);
// We need to manually add the system cancel event
if (SubscriptionTransitionType.CANCEL.equals(event.getTransitionType())) {
final BusinessSubscriptionTransitionModelDao systemCancelTransition = new BusinessSubscriptionTransitionModelDao(
event.getTotalOrdering(),
bundleId,
bundle.getExternalKey(),
bundle.getAccountId(),
account.getExternalKey(),
subscription.getId(),
// Note! The system cancel event requested time is the effective time when the subscription
// is cancelled, which is the effective time of the cancel event
event.getEffectiveTransitionTime(),
new BusinessSubscriptionEvent(BusinessSubscriptionEvent.EventType.SYSTEM_CANCEL, businessEvent.getCategory()),
prevSubscription,
nextSubscription
);
transactional.createTransition(systemCancelTransition, context);
transitions.add(systemCancelTransition);
log.info("Adding transition {}", systemCancelTransition);
}
}
}
log.info("Finished rebuilding transitions for bundle id {}", bundleId);
return null;
}
});
}
private BusinessSubscriptionEvent getBusinessSubscriptionFromEvent(final SubscriptionInternalEvent event) throws AccountApiException, EntitlementUserApiException {
switch (event.getTransitionType()) {
// A subscription enters either through migration or as newly created subscription
case MIGRATE_ENTITLEMENT:
return subscriptionMigrated(event);
case CREATE:
return subscriptionCreated(event);
case RE_CREATE:
return subscriptionRecreated(event);
case TRANSFER:
return subscriptionTransfered(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 subscriptionMigrated(final SubscriptionInternalEvent created) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionMigrated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionCreated(final SubscriptionInternalEvent created) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionCreated(created.getNextPlan(), catalogService.getFullCatalog(), created.getEffectiveTransitionTime(), created.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionRecreated(final SubscriptionInternalEvent recreated) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionRecreated(recreated.getNextPlan(), catalogService.getFullCatalog(), recreated.getEffectiveTransitionTime(), recreated.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionTransfered(final SubscriptionInternalEvent transfered) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionTransfered(transfered.getNextPlan(), catalogService.getFullCatalog(), transfered.getEffectiveTransitionTime(), transfered.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionCancelled(final SubscriptionInternalEvent cancelled) throws AccountApiException, EntitlementUserApiException {
// cancelled.getNextPlan() is null here - need to look at the previous one to create the correct event name
return BusinessSubscriptionEvent.subscriptionCancelled(cancelled.getPreviousPlan(), catalogService.getFullCatalog(), cancelled.getEffectiveTransitionTime(), cancelled.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionChanged(final SubscriptionInternalEvent changed) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionChanged(changed.getNextPlan(), catalogService.getFullCatalog(), changed.getEffectiveTransitionTime(), changed.getSubscriptionStartDate());
}
private BusinessSubscriptionEvent subscriptionPhaseChanged(final SubscriptionInternalEvent phaseChanged) throws AccountApiException, EntitlementUserApiException {
return BusinessSubscriptionEvent.subscriptionPhaseChanged(phaseChanged.getNextPlan(), phaseChanged.getNextState(), catalogService.getFullCatalog(), phaseChanged.getEffectiveTransitionTime(), phaseChanged.getSubscriptionStartDate());
}
private BusinessSubscription createNextBusinessSubscription(final EffectiveSubscriptionInternalEvent 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(),
catalogService.getFullCatalog());
}
return nextSubscription;
}
private BusinessSubscription createPreviousBusinessSubscription(final EffectiveSubscriptionInternalEvent event,
final BusinessSubscriptionEvent businessEvent,
final ArrayList<BusinessSubscriptionTransitionModelDao> transitions,
final Currency currency) {
if (BusinessSubscriptionEvent.EventType.MIGRATE.equals(businessEvent.getEventType()) ||
BusinessSubscriptionEvent.EventType.ADD.equals(businessEvent.getEventType()) ||
BusinessSubscriptionEvent.EventType.RE_ADD.equals(businessEvent.getEventType()) ||
BusinessSubscriptionEvent.EventType.TRANSFER.equals(businessEvent.getEventType())) {
return null;
}
final BusinessSubscriptionTransitionModelDao prevTransition = getPreviousBusinessSubscriptionTransitionForEvent(event, transitions);
return new BusinessSubscription(event.getPreviousPriceList(), event.getPreviousPlan(), event.getPreviousPhase(),
currency, prevTransition.getNextSubscription().getStartDate(), event.getPreviousState(),
catalogService.getFullCatalog());
}
private BusinessSubscriptionTransitionModelDao getPreviousBusinessSubscriptionTransitionForEvent(final EffectiveSubscriptionInternalEvent event,
final ArrayList<BusinessSubscriptionTransitionModelDao> transitions) {
BusinessSubscriptionTransitionModelDao transition = null;
for (final BusinessSubscriptionTransitionModelDao candidate : transitions) {
final BusinessSubscription nextSubscription = candidate.getNextSubscription();
if (nextSubscription == null || !nextSubscription.getStartDate().isBefore(event.getEffectiveTransitionTime())) {
continue;
}
if (candidate.getSubscriptionId().equals(event.getSubscriptionId())) {
transition = candidate;
}
}
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);
}
return transition;
}
}