public DefaultPlanPhase setPlan(final Plan plan) { this.planName = plan.getName(); this.product = plan.getProduct(); return this; }
private Iterable<Plan> toDefaultPlans(final StaticCatalog staticCatalog, final Iterable<Plan> input) { if (tmpDefaultPlans == null) { final Map<String, Plan> map = new HashMap<String, Plan>(); for (final Plan plan : input) map.put(plan.getName(), toDefaultPlan(staticCatalog, plan)); tmpDefaultPlans = map; } return tmpDefaultPlans.values(); }
@Override public boolean apply(final Plan input) { return input.getName().equals(planName); } });
private int countCurrentAddOnsWithSamePlanName(final Iterable<Plan> entitlementsPlans, final Plan currentPlan) { int countCurrentAddOns = 0; for (final Plan plan : entitlementsPlans) { if (plan.getName().equalsIgnoreCase(currentPlan.getName()) && plan.getProduct().getCategory() != null && ProductCategory.ADD_ON.equals(plan.getProduct().getCategory())) { countCurrentAddOns++; } } return countCurrentAddOns; } }
private List<TimedPhase> getTimedPhaseOnCreate(final DateTime subscriptionStartDate, final DateTime bundleStartDate, final Plan plan, @Nullable final PhaseType initialPhase, final Catalog catalog, final DateTime catalogEffectiveDate, final InternalTenantContext context) throws CatalogApiException, SubscriptionBaseApiException { final PlanSpecifier planSpecifier = new PlanSpecifier(plan.getName()); final DateTime planStartDate; final PlanAlignmentCreate alignment = catalog.planCreateAlignment(planSpecifier, catalogEffectiveDate, subscriptionStartDate); switch (alignment) { case START_OF_SUBSCRIPTION: planStartDate = subscriptionStartDate; break; case START_OF_BUNDLE: planStartDate = bundleStartDate; break; default: throw new SubscriptionBaseError(String.format("Unknown PlanAlignmentCreate %s", alignment)); } return getPhaseAlignments(plan, initialPhase, planStartDate); }
private enum WhichPhase { CURRENT, NEXT }
public int countExistingAddOnsWithSamePlanName(final Iterable<DefaultSubscriptionBase> subscriptionsForBundle, final String planName) { int countExistingAddOns = 0; for (final SubscriptionBase subscription : subscriptionsForBundle) { if (subscription.getCurrentPlan().getName().equalsIgnoreCase(planName) && subscription.getLastActiveProduct().getCategory() != null && ProductCategory.ADD_ON.equals(subscription.getLastActiveProduct().getCategory())) { countExistingAddOns++; } } return countExistingAddOns; } }
@Override public List<Listing> getAvailableBasePlanListings() { final List<Listing> availBasePlans = new ArrayList<Listing>(); for (final Plan plan : getCurrentPlans()) { if (plan.getProduct().getCategory().equals(ProductCategory.BASE)) { for (final PriceList priceList : getPriceLists().getAllPriceLists()) { for (final Plan priceListPlan : priceList.getPlans()) { if (priceListPlan.getName().equals(plan.getName()) && priceListPlan.getProduct().getName().equals(plan.getProduct().getName())) { availBasePlans.add(new DefaultListing(priceListPlan, priceList)); } } } } } return availBasePlans; }
private Plan toDefaultPlan(final StaticCatalog staticCatalog, final Plan input) { if (tmpDefaultPlans != null) { final Plan existingPlan = tmpDefaultPlans.get(input.getName()); if (existingPlan == null) throw new IllegalStateException("Unknown plan " + input.getName()); return existingPlan; } final DefaultPlan result = new DefaultPlan(); result.setName(input.getName()); result.setPrettyName(input.getPrettyName()); result.setRecurringBillingMode(input.getRecurringBillingMode()); result.setEffectiveDateForExistingSubscriptions(input.getEffectiveDateForExistingSubscriptions()); result.setFinalPhase(toDefaultPlanPhase(input.getFinalPhase())); result.setInitialPhases(toDefaultPlanPhases(ImmutableList.copyOf(input.getInitialPhases()))); result.setPlansAllowedInBundle(input.getPlansAllowedInBundle()); result.setProduct(toDefaultProduct(input.getProduct())); result.setPriceListName(input.getPriceListName()); return result; }
public void populatePriceLists() { final Collection<Plan> plans = getCurrentPlans(); final DefaultPriceList[] priceList = new DefaultPriceList[plans.size() - 1]; int i = 1; final Iterator<Plan> it = plans.iterator(); final Plan initialPlan = it.next(); while (it.hasNext()) { final Plan plan = it.next(); priceList[i - 1] = new DefaultPriceList(new DefaultPlan[]{(DefaultPlan) plan}, plan.getName() + "-pl"); i++; } final DefaultPriceListSet set = new DefaultPriceListSet(new PriceListDefault(new DefaultPlan[]{(DefaultPlan) initialPlan}), priceList); setPriceLists(set); }
public void checkAddonCreationRights(final SubscriptionBase baseSubscription, final Plan targetAddOnPlan, final DateTime requestedDate, final InternalTenantContext context) throws SubscriptionBaseApiException { if (baseSubscription.getState() == EntitlementState.CANCELLED || (baseSubscription.getState() == EntitlementState.PENDING && context.toLocalDate(baseSubscription.getStartDate()).compareTo(context.toLocalDate(requestedDate)) < 0)) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_BP_NON_ACTIVE, targetAddOnPlan.getName()); } final Plan currentOrPendingPlan = baseSubscription.getCurrentOrPendingPlan(); final Product baseProduct = currentOrPendingPlan.getProduct(); if (isAddonIncluded(baseProduct, targetAddOnPlan)) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_ALREADY_INCLUDED, targetAddOnPlan.getName(), currentOrPendingPlan.getProduct().getName()); } if (!isAddonAvailable(baseProduct, targetAddOnPlan)) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_AO_NOT_AVAILABLE, targetAddOnPlan.getName(), currentOrPendingPlan.getProduct().getName()); } }
@Override public boolean cancelWithPolicyNoValidationAndCatalog(final Iterable<DefaultSubscriptionBase> subscriptions, final BillingActionPolicy policy, final Catalog catalog, final InternalCallContext context) throws SubscriptionBaseApiException { final Map<DefaultSubscriptionBase, DateTime> subscriptionsWithEffectiveDate = new HashMap<DefaultSubscriptionBase, DateTime>(); try { for (final DefaultSubscriptionBase subscription : subscriptions) { final BillingAlignment billingAlignment = (subscription.getState() == EntitlementState.PENDING ? null : catalog.billingAlignment(new PlanPhaseSpecifier(subscription.getLastActivePlan().getName(), subscription.getLastActivePhase().getPhaseType()), clock.getUTCNow(), subscription.getStartDate())); final Integer accountBillCycleDayLocal = accountInternalApi.getBCD(context); final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, billingAlignment, accountBillCycleDayLocal, context); subscriptionsWithEffectiveDate.put(subscription, effectiveDate); } return doCancelPlan(subscriptionsWithEffectiveDate, catalog, context); } catch (final CatalogApiException e) { throw new SubscriptionBaseApiException(e); } catch (final AccountApiException e) { throw new SubscriptionBaseApiException(e); } }
@Override public boolean cancel(final DefaultSubscriptionBase subscription, final CallContext context) throws SubscriptionBaseApiException { final EntitlementState currentState = subscription.getState(); if (currentState == EntitlementState.CANCELLED) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CANCEL_BAD_STATE, subscription.getId(), currentState); } final Plan currentPlan = subscription.getCurrentOrPendingPlan(); final PlanPhaseSpecifier planPhase = new PlanPhaseSpecifier(currentPlan.getName(), null); try { final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context); final Catalog fullCatalog = catalogInternalApi.getFullCatalog(true, true, internalCallContext); final BillingActionPolicy policy = fullCatalog.planCancelPolicy(planPhase, clock.getUTCNow(), subscription.getStartDate()); Preconditions.checkState(policy != BillingActionPolicy.START_OF_TERM, "A default START_OF_TERM policy is not availaible"); final DateTime effectiveDate = subscription.getPlanChangeEffectiveDate(policy, null, -1, null); return doCancelPlan(ImmutableMap.<DefaultSubscriptionBase, DateTime>of(subscription, effectiveDate), fullCatalog, internalCallContext); } catch (final CatalogApiException e) { throw new SubscriptionBaseApiException(e); } }
@Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/842") public void testCreateAmbiguousPlan() throws CatalogApiException { final DateTime now = clock.getUTCNow(); final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly-12345", "Foo", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of()); final CatalogUpdater catalogUpdater = new CatalogUpdater(now, desc.getCurrency()); catalogUpdater.addSimplePlanDescriptor(desc); final StandaloneCatalog catalog = catalogUpdater.getCatalog(); assertEquals(catalog.getCurrentPlans().size(), 1); final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog, priceOverride, internalCallContext.getTenantRecordId(), internalCallContextFactory); final Plan plan = catalog.findCurrentPlan("foo-monthly-12345"); assertEquals(plan.getName(), "foo-monthly-12345"); // Verify PriceOverride logic final Plan plan2 = standaloneCatalogWithPriceOverride.findCurrentPlan("foo-monthly-12345"); assertEquals(plan2.getName(), "foo-monthly-12345"); final PlanPhase planPhase = catalog.findCurrentPhase("foo-monthly-12345-evergreen"); assertEquals(planPhase.getName(), "foo-monthly-12345-evergreen"); // Verify PriceOverride logic final PlanPhase phase2 = standaloneCatalogWithPriceOverride.findCurrentPhase("foo-monthly-12345-evergreen"); assertEquals(phase2.getName(), "foo-monthly-12345-evergreen"); }
@Override public PlanChangeResult getPlanChangeResult(final DefaultSubscriptionBase subscription, final PlanSpecifier toPlanPhase, final DateTime effectiveDate, final TenantContext context) throws SubscriptionBaseApiException { final PlanChangeResult planChangeResult; try { final InternalTenantContext internalCallContext = createTenantContextFromBundleId(subscription.getBundleId(), context); final Plan currentPlan = subscription.getCurrentOrPendingPlan(); final PlanPhaseSpecifier fromPlanPhase = new PlanPhaseSpecifier(currentPlan.getName(), subscription.getCurrentOrPendingPhase().getPhaseType()); planChangeResult = catalogInternalApi.getFullCatalog(true, true, internalCallContext).planChange(fromPlanPhase, toPlanPhase, effectiveDate, subscription.getStartDate()); } catch (final CatalogApiException e) { throw new SubscriptionBaseApiException(e); } return planChangeResult; }
public DefaultPlanPhase(final Plan parentPlan, final DefaultPlanPhase in, @Nullable final PlanPhasePriceOverride override) { this.type = in.getPhaseType(); this.duration = (DefaultDuration) in.getDuration(); this.fixed = override != null && override.getFixedPrice() != null ? new DefaultFixed((DefaultFixed) in.getFixed(), override) : (DefaultFixed) in.getFixed(); this.recurring = override != null && override.getRecurringPrice() != null ? new DefaultRecurring((DefaultRecurring) in.getRecurring(), override) : (DefaultRecurring) in.getRecurring(); this.usages = new DefaultUsage[in.getUsages().length]; for (int i = 0; i < in.getUsages().length; i++) { final Usage curUsage = in.getUsages()[i]; if (override != null && override.getUsagePriceOverrides() != null) { final UsagePriceOverride usagePriceOverride = Iterables.tryFind(override.getUsagePriceOverrides(), new Predicate<UsagePriceOverride>() { @Override public boolean apply(final UsagePriceOverride input) { return input != null && input.getName().equals(curUsage.getName()); } }).orNull(); usages[i] = (usagePriceOverride != null) ? new DefaultUsage(in.getUsages()[i], usagePriceOverride, override.getCurrency()) : (DefaultUsage) curUsage; } else { usages[i] = (DefaultUsage) curUsage; } } this.planName = parentPlan.getName(); this.product = parentPlan.getProduct(); }
@Override public DateTime getDryRunChangePlanEffectiveDate(final SubscriptionBase subscription, final EntitlementSpecifier spec, final DateTime requestedDateWithMs, final BillingActionPolicy requestedPolicy, final InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException { final TenantContext tenantContext = internalCallContextFactory.createTenantContext(context); final CallContext callContext = internalCallContextFactory.createCallContext(context); // verify the number of subscriptions (of the same kind) allowed per bundle final Catalog catalog = catalogInternalApi.getFullCatalog(true, true, context); final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : null; final DateTime effectiveCatalogDate = effectiveDate != null ? effectiveDate : context.getCreatedDate(); final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(spec.getOverrides(), callContext); final Plan plan = catalog.createOrFindPlan(spec.getPlanPhaseSpecifier(), overridesWithContext, effectiveCatalogDate, subscription.getStartDate()); if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) { if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0 && addonUtils.countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(subscription.getBundleId(), null, catalog, addonUtils, callContext, context), plan.getName()) >= plan.getPlansAllowedInBundle()) { // the plan can be changed to the new value, because it has reached its limit by bundle throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, plan.getName()); } } return apiService.dryRunChangePlan((DefaultSubscriptionBase) subscription, spec, effectiveDate, requestedPolicy, tenantContext); }
@Test(groups = "fast") public void testAddExistingPlanWithNewCurrency() throws Exception { final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().size(), 1); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getName(), new PriceListDefault().getName()); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getPlans().size(), 3); final CatalogUpdater catalogUpdater = new CatalogUpdater(originalCatalog); final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("standard-monthly", "Standard", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 30, TimeUnit.DAYS, ImmutableList.<String>of()); catalogUpdater.addSimplePlanDescriptor(desc); final StandaloneCatalog catalog = catalogUpdater.getCatalog(); final Plan plan = catalog.findCurrentPlan("standard-monthly"); assertEquals(plan.getName(), "standard-monthly"); assertEquals(plan.getInitialPhases().length, 1); assertEquals(plan.getInitialPhases()[0].getPhaseType(), PhaseType.TRIAL); assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrices().length, 0); assertEquals(plan.getInitialPhases()[0].getFixed().getPrice().getPrice(Currency.EUR), BigDecimal.ZERO); assertEquals(plan.getInitialPhases()[0].getName(), "standard-monthly-trial"); assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN); assertNull(plan.getFinalPhase().getFixed()); assertEquals(plan.getFinalPhase().getName(), "standard-monthly-evergreen"); assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY); assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 3); assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrice(Currency.EUR), BigDecimal.TEN); }
@Test(groups = "fast") public void testAddPlanOnExistingCatalog() throws Exception { final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().size(), 1); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getName(), new PriceListDefault().getName()); assertEquals(originalCatalog.getPriceLists().getAllPriceLists().get(0).getPlans().size(), 3); final CatalogUpdater catalogUpdater = new CatalogUpdater(originalCatalog); final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("standard-annual", "Standard", ProductCategory.BASE, Currency.USD, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of()); catalogUpdater.addSimplePlanDescriptor(desc); final StandaloneCatalog catalog = catalogUpdater.getCatalog(); final Plan plan = catalog.findCurrentPlan("standard-annual"); assertEquals(plan.getName(), "standard-annual"); assertEquals(plan.getInitialPhases().length, 0); assertEquals(plan.getFinalPhase().getPhaseType(), PhaseType.EVERGREEN); assertNull(plan.getFinalPhase().getFixed()); assertEquals(plan.getFinalPhase().getName(), "standard-annual-evergreen"); assertEquals(plan.getFinalPhase().getRecurring().getBillingPeriod(), BillingPeriod.MONTHLY); assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices().length, 1); assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getValue(), BigDecimal.TEN); assertEquals(plan.getFinalPhase().getRecurring().getRecurringPrice().getPrices()[0].getCurrency(), Currency.USD); assertEquals(catalog.getPriceLists().getAllPriceLists().size(), 1); final PriceList priceList = catalog.getPriceLists().getAllPriceLists().get(0); assertEquals(priceList.getName(), new PriceListDefault().getName()); assertEquals(priceList.getPlans().size(), 4); }
private void doChangePlan(final DefaultSubscriptionBase subscription, final EntitlementSpecifier spec, final DateTime effectiveDate, final CallContext context) throws SubscriptionBaseApiException, CatalogApiException { final InternalCallContext internalCallContext = createCallContextFromBundleId(subscription.getBundleId(), context); final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(spec.getOverrides(), context); final Catalog fullCatalog = catalogInternalApi.getFullCatalog(true, true, internalCallContext); final PlanPhaseSpecifier planPhaseSpecifier = spec.getPlanPhaseSpecifier(); final Plan newPlan = fullCatalog.createOrFindPlan(planPhaseSpecifier, overridesWithContext, effectiveDate, subscription.getStartDate()); final PhaseType initialPhaseType = planPhaseSpecifier.getPhaseType(); if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(newPlan.getProduct().getCategory().toString())) { if (newPlan.getPlansAllowedInBundle() != -1 && newPlan.getPlansAllowedInBundle() > 0 && addonUtils.countExistingAddOnsWithSamePlanName(dao.getSubscriptions(subscription.getBundleId(), null, fullCatalog, internalCallContext), newPlan.getName()) >= newPlan.getPlansAllowedInBundle()) { // the plan can be changed to the new value, because it has reached its limit by bundle throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE, newPlan.getName()); } } if (newPlan.getProduct().getCategory() != subscription.getCategory()) { throw new SubscriptionBaseApiException(ErrorCode.SUB_CHANGE_INVALID, subscription.getId()); } final List<DefaultSubscriptionBase> addOnSubscriptionsToBeCancelled = new ArrayList<DefaultSubscriptionBase>(); final List<SubscriptionBaseEvent> addOnCancelEvents = new ArrayList<SubscriptionBaseEvent>(); final List<SubscriptionBaseEvent> changeEvents = getEventsOnChangePlan(subscription, newPlan, newPlan.getPriceListName(), effectiveDate, true, addOnSubscriptionsToBeCancelled, addOnCancelEvents, initialPhaseType, spec.getBillCycleDay(), fullCatalog, internalCallContext); dao.changePlan(subscription, changeEvents, addOnSubscriptionsToBeCancelled, addOnCancelEvents, fullCatalog, internalCallContext); subscription.rebuildTransitions(dao.getEventsForSubscription(subscription.getId(), internalCallContext), fullCatalog); }