Java tutorial
/* * Copyright 2010-2013 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 org.killbill.billing.subscription.api; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.killbill.billing.ErrorCode; import org.killbill.billing.callcontext.InternalCallContext; import org.killbill.billing.callcontext.InternalTenantContext; import org.killbill.billing.catalog.api.BillingActionPolicy; import org.killbill.billing.catalog.api.Catalog; import org.killbill.billing.catalog.api.CatalogApiException; import org.killbill.billing.catalog.api.Plan; import org.killbill.billing.catalog.api.PlanChangeResult; import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext; import org.killbill.billing.catalog.api.PlanPhaseSpecifier; import org.killbill.billing.catalog.api.ProductCategory; import org.killbill.billing.entitlement.api.Entitlement.EntitlementState; import org.killbill.billing.entitlement.api.EntitlementSpecifier; import org.killbill.billing.invoice.api.DryRunArguments; import org.killbill.billing.subscription.api.svcs.DefaultPlanPhasePriceOverridesWithCallContext; import org.killbill.billing.subscription.api.svcs.DefaultSubscriptionInternalApi; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase; import org.killbill.billing.subscription.api.user.DefaultSubscriptionBaseBundle; import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException; import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle; import org.killbill.billing.subscription.api.user.SubscriptionBuilder; import org.killbill.billing.subscription.engine.addon.AddonUtils; import org.killbill.billing.subscription.engine.dao.SubscriptionDao; import org.killbill.billing.subscription.events.SubscriptionBaseEvent; import org.killbill.billing.subscription.exceptions.SubscriptionBaseError; import org.killbill.billing.util.UUIDs; import org.killbill.billing.util.cache.CacheController; import org.killbill.billing.util.callcontext.TenantContext; import org.killbill.clock.Clock; import com.google.common.base.Function; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; public class SubscriptionApiBase { protected final SubscriptionDao dao; protected final SubscriptionBaseApiService apiService; protected final Clock clock; public SubscriptionApiBase(final SubscriptionDao dao, final SubscriptionBaseApiService apiService, final Clock clock) { this.dao = dao; this.apiService = apiService; this.clock = clock; } protected SubscriptionBaseBundle getActiveBundleForKey(final String bundleKey, final Catalog catalog, final InternalTenantContext context) throws CatalogApiException { final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context); for (final SubscriptionBaseBundle cur : existingBundles) { final List<DefaultSubscriptionBase> subscriptions = dao.getSubscriptions(cur.getId(), ImmutableList.<SubscriptionBaseEvent>of(), catalog, context); for (final SubscriptionBase s : subscriptions) { if (s.getCategory() == ProductCategory.ADD_ON) { continue; } if (s.getEndDate() == null || s.getEndDate().compareTo(clock.getUTCNow()) > 0) { return cur; } } } return null; } protected SubscriptionBase getBaseSubscription(final UUID bundleId, final Catalog catalog, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException { final SubscriptionBase result = dao.getBaseSubscription(bundleId, catalog, context); if (result == null) { throw new SubscriptionBaseApiException(ErrorCode.SUB_GET_NO_SUCH_BASE_SUBSCRIPTION, bundleId); } return createSubscriptionForApiUse(result); } protected List<DefaultSubscriptionBase> getSubscriptionsForBundle(final UUID bundleId, @Nullable final DryRunArguments dryRunArguments, final Catalog catalog, final AddonUtils addonUtils, final TenantContext tenantContext, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException { final List<SubscriptionBaseEvent> outputDryRunEvents = new ArrayList<SubscriptionBaseEvent>(); final List<DefaultSubscriptionBase> outputSubscriptions = new ArrayList<DefaultSubscriptionBase>(); populateDryRunEvents(bundleId, dryRunArguments, outputDryRunEvents, outputSubscriptions, catalog, addonUtils, tenantContext, context); final List<DefaultSubscriptionBase> result = dao.getSubscriptions(bundleId, outputDryRunEvents, catalog, context); if (result != null && !result.isEmpty()) { outputSubscriptions.addAll(result); } Collections.sort(outputSubscriptions, DefaultSubscriptionInternalApi.SUBSCRIPTIONS_COMPARATOR); return createSubscriptionsForApiUse(outputSubscriptions); } private void populateDryRunEvents(@Nullable final UUID bundleId, @Nullable final DryRunArguments dryRunArguments, final Collection<SubscriptionBaseEvent> outputDryRunEvents, final Collection<DefaultSubscriptionBase> outputSubscriptions, final Catalog catalog, final AddonUtils addonUtils, final TenantContext tenantContext, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException { if (dryRunArguments == null || dryRunArguments.getAction() == null) { return; } final DateTime utcNow = clock.getUTCNow(); List<SubscriptionBaseEvent> dryRunEvents = null; final EntitlementSpecifier entitlementSpecifier = dryRunArguments.getEntitlementSpecifier(); final PlanPhaseSpecifier inputSpec = entitlementSpecifier.getPlanPhaseSpecifier(); final boolean isInputSpecNullOrEmpty = inputSpec == null || (inputSpec.getPlanName() == null && inputSpec.getProductName() == null && inputSpec.getBillingPeriod() == null); // Create an overridesWithContext with a null context to indicate this is dryRun and no price overridden plan should be created. final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext( entitlementSpecifier.getOverrides(), null); final Plan plan = isInputSpecNullOrEmpty ? null : catalog.createOrFindPlan(inputSpec, overridesWithContext, utcNow); switch (dryRunArguments.getAction()) { case START_BILLING: final SubscriptionBase baseSubscription = dao.getBaseSubscription(bundleId, catalog, context); final DateTime startEffectiveDate = dryRunArguments.getEffectiveDate() != null ? context.toUTCDateTime(dryRunArguments.getEffectiveDate()) : utcNow; final DateTime bundleStartDate = getBundleStartDateWithSanity(bundleId, baseSubscription, plan, startEffectiveDate, addonUtils, context); final UUID subscriptionId = UUIDs.randomUUID(); dryRunEvents = apiService.getEventsOnCreation(subscriptionId, startEffectiveDate, bundleStartDate, plan, inputSpec.getPhaseType(), plan.getPriceListName(), startEffectiveDate, entitlementSpecifier.getBillCycleDay(), catalog, context); final SubscriptionBuilder builder = new SubscriptionBuilder().setId(subscriptionId) .setBundleId(bundleId).setBundleExternalKey(null).setCategory(plan.getProduct().getCategory()) .setBundleStartDate(bundleStartDate).setAlignStartDate(startEffectiveDate); final DefaultSubscriptionBase newSubscription = new DefaultSubscriptionBase(builder, apiService, clock); newSubscription.rebuildTransitions(dryRunEvents, catalog); outputSubscriptions.add(newSubscription); break; case CHANGE: final DefaultSubscriptionBase subscriptionForChange = (DefaultSubscriptionBase) dao .getSubscriptionFromId(dryRunArguments.getSubscriptionId(), catalog, context); DateTime changeEffectiveDate = getDryRunEffectiveDate(dryRunArguments.getEffectiveDate(), subscriptionForChange, context); if (changeEffectiveDate == null) { BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy(); if (policy == null) { final PlanChangeResult planChangeResult = apiService.getPlanChangeResult(subscriptionForChange, inputSpec, utcNow, tenantContext); policy = planChangeResult.getPolicy(); } // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail changeEffectiveDate = subscriptionForChange.getPlanChangeEffectiveDate(policy, null, -1, context); } dryRunEvents = apiService.getEventsOnChangePlan(subscriptionForChange, plan, plan.getPriceListName(), changeEffectiveDate, true, entitlementSpecifier.getBillCycleDay(), catalog, context); break; case STOP_BILLING: final DefaultSubscriptionBase subscriptionForCancellation = (DefaultSubscriptionBase) dao .getSubscriptionFromId(dryRunArguments.getSubscriptionId(), catalog, context); DateTime cancelEffectiveDate = getDryRunEffectiveDate(dryRunArguments.getEffectiveDate(), subscriptionForCancellation, context); if (dryRunArguments.getEffectiveDate() == null) { BillingActionPolicy policy = dryRunArguments.getBillingActionPolicy(); if (policy == null) { final Plan currentPlan = subscriptionForCancellation.getCurrentPlan(); final PlanPhaseSpecifier spec = new PlanPhaseSpecifier(currentPlan.getName(), subscriptionForCancellation.getCurrentPhase().getPhaseType()); policy = catalog.planCancelPolicy(spec, clock.getUTCNow(), subscriptionForCancellation.getStartDate()); } // We pass null for billingAlignment, accountTimezone, account BCD because this is not available which means that dryRun with START_OF_TERM BillingPolicy will fail cancelEffectiveDate = subscriptionForCancellation.getPlanChangeEffectiveDate(policy, null, -1, context); } dryRunEvents = apiService.getEventsOnCancelPlan(subscriptionForCancellation, cancelEffectiveDate, true, catalog, context); break; default: throw new IllegalArgumentException("Unexpected dryRunArguments action " + dryRunArguments.getAction()); } if (dryRunEvents != null && !dryRunEvents.isEmpty()) { outputDryRunEvents.addAll(dryRunEvents); } } private DateTime getDryRunEffectiveDate(@Nullable final LocalDate inputDate, final SubscriptionBase subscription, final InternalTenantContext context) { if (inputDate == null) { return null; } // We first use context account reference time to get a candidate) final DateTime tmp = context.toUTCDateTime(inputDate); // If we realize that the candidate is on the same LocalDate boundary as the subscription startDate but a bit prior we correct it to avoid weird things down the line if (inputDate.compareTo(context.toLocalDate(subscription.getStartDate())) == 0 && tmp.compareTo(subscription.getStartDate()) < 0) { return subscription.getStartDate(); } else { return tmp; } } protected DateTime getBundleStartDateWithSanity(final UUID bundleId, @Nullable final SubscriptionBase baseSubscription, final Plan plan, final DateTime effectiveDate, final AddonUtils addonUtils, final InternalTenantContext context) throws SubscriptionBaseApiException, CatalogApiException { switch (plan.getProduct().getCategory()) { case BASE: if (baseSubscription != null && (baseSubscription.getState() == EntitlementState.ACTIVE || baseSubscription.getState() == EntitlementState.PENDING)) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId); } return effectiveDate; case ADD_ON: if (baseSubscription == null) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BP, bundleId); } if (effectiveDate.isBefore(baseSubscription.getStartDate())) { throw new SubscriptionBaseApiException(ErrorCode.SUB_INVALID_REQUESTED_DATE, effectiveDate.toString(), baseSubscription.getStartDate().toString()); } addonUtils.checkAddonCreationRights(baseSubscription, plan, effectiveDate, context); return baseSubscription.getStartDate(); case STANDALONE: if (baseSubscription != null) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_BP_EXISTS, bundleId); } // Not really but we don't care, there is no alignment for STANDALONE subscriptions return effectiveDate; default: throw new SubscriptionBaseError(String.format("Can't create subscription of type %s", plan.getProduct().getCategory().toString())); } } protected SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final boolean renameCancelledBundleIfExist, final Catalog catalog, final CacheController<UUID, UUID> accountIdCacheController, final InternalCallContext context) throws SubscriptionBaseApiException { final DateTime now = context.getCreatedDate(); final DefaultSubscriptionBaseBundle bundle = new DefaultSubscriptionBaseBundle(bundleKey, accountId, now, now, now, now); if (null != bundleKey && bundleKey.length() > 255) { throw new SubscriptionBaseApiException(ErrorCode.EXTERNAL_KEY_LIMIT_EXCEEDED); } final SubscriptionBaseBundle subscriptionBundle = dao.createSubscriptionBundle(bundle, catalog, renameCancelledBundleIfExist, context); accountIdCacheController.putIfAbsent(bundle.getId(), accountId); return subscriptionBundle; } protected List<DefaultSubscriptionBase> createSubscriptionsForApiUse( final Collection<DefaultSubscriptionBase> internalSubscriptions) { return new ArrayList<DefaultSubscriptionBase>(Collections2.transform(internalSubscriptions, new Function<SubscriptionBase, DefaultSubscriptionBase>() { @Override public DefaultSubscriptionBase apply(final SubscriptionBase subscription) { return createSubscriptionForApiUse(subscription); } })); } protected DefaultSubscriptionBase createSubscriptionForApiUse(final SubscriptionBase internalSubscription) { return new DefaultSubscriptionBase((DefaultSubscriptionBase) internalSubscription, apiService, clock); } protected DefaultSubscriptionBase createSubscriptionForApiUse(final SubscriptionBuilder builder, final List<SubscriptionBaseEvent> events, final Catalog catalog, final InternalTenantContext context) throws CatalogApiException { final DefaultSubscriptionBase subscription = new DefaultSubscriptionBase(builder, apiService, clock); if (!events.isEmpty()) { subscription.rebuildTransitions(events, catalog); } return subscription; } }