Details
diff --git a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
index 234f9f3..fada42e 100644
--- a/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseInternalApi.java
@@ -89,7 +89,7 @@ public interface SubscriptionBaseInternalApi {
public List<EffectiveSubscriptionInternalEvent> getBillingTransitions(SubscriptionBase subscription, InternalTenantContext context);
- public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, InternalTenantContext context) throws SubscriptionBaseApiException;
+ public DateTime getDryRunChangePlanEffectiveDate(SubscriptionBase subscription, PlanSpecifier spec, DateTime requestedDate, BillingActionPolicy policy, List<PlanPhasePriceOverride> overrides, InternalCallContext context) throws SubscriptionBaseApiException, CatalogApiException;
public List<EntitlementAOStatusDryRun> getDryRunChangePlanStatus(UUID subscriptionId, @Nullable String baseProductName,
DateTime requestedDate, InternalTenantContext context) throws SubscriptionBaseApiException;
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
index d741ae9..17d84b4 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestSubscription.java
@@ -313,6 +313,45 @@ public class TestSubscription extends TestIntegrationBase {
}
@Test(groups = "slow")
+ public void testChangePlanWithLimitException() throws Exception {
+ final LocalDate initialDate = new LocalDate(2015, 10, 1);
+ clock.setDay(initialDate);
+
+ final Account account = createAccountWithNonOsgiPaymentMethod(getAccountData(1));
+
+ final PlanPhaseSpecifier addOnSpec1 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec2 = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ final PlanPhaseSpecifier addOnSpec3 = new PlanPhaseSpecifier("Telescopic-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+ // Create base subscription
+ final Entitlement baseEntitlement = createBaseEntitlementAndCheckForCompletion(account.getId(), account.getExternalKey(), "Shotgun", ProductCategory.BASE, BillingPeriod.MONTHLY, NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE);
+ assertNotNull(baseEntitlement);
+
+ // Create first add_on subscription
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec1, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Create second add_on subscription with the same plan
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec2, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Create third add_on subscription with another plan
+ busHandler.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.INVOICE, NextEvent.PAYMENT, NextEvent.INVOICE_PAYMENT);
+ Entitlement addOn3 = entitlementApi.addEntitlement(baseEntitlement.getBundleId(), addOnSpec3, null, initialDate, initialDate, false, ImmutableList.<PluginProperty>of(), callContext);
+ assertListenerStatus();
+
+ // Trying to change the plan of the third add_on to 'Laser-Scope' plan, should throw an exception (the limit is 2 for this plan)
+ try {
+ final PlanPhaseSpecifier addOnSpecChangedPlan = new PlanPhaseSpecifier("Laser-Scope", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+ addOn3.changePlan(addOnSpecChangedPlan, null, ImmutableList.<PluginProperty>of(), callContext);
+ } catch (final EntitlementApiException e) {
+ assertEquals(e.getCode(), ErrorCode.SUB_CHANGE_AO_MAX_PLAN_ALLOWED_BY_BUNDLE.getCode());
+ }
+ }
+
+ @Test(groups = "slow")
public void testCancelFutureSubscriptionWithPolicy() throws Exception {
final LocalDate initialDate = new LocalDate(2015, 9, 1);
clock.setDay(initialDate);
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
index 3e5fa05..1f84737 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlement.java
@@ -35,6 +35,7 @@ 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.BillingPeriod;
+import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.Plan;
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
@@ -559,9 +560,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime effectiveChangeDate;
try {
- effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, context);
+ effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, null, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
@@ -624,9 +627,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime resultingEffectiveDate;
try {
- resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, context);
+ resultingEffectiveDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, effectiveChangeDate, null, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
@@ -689,9 +694,11 @@ public class DefaultEntitlement extends EntityBase implements Entitlement {
final DateTime effectiveChangeDate;
try {
- effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, context);
+ effectiveChangeDate = subscriptionInternalApi.getDryRunChangePlanEffectiveDate(getSubscriptionBase(), spec, null, actionPolicy, overrides, context);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e, e.getCode(), e.getMessage());
+ } catch (final CatalogApiException e) {
+ throw new EntitlementApiException(e, e.getCode(), e.getMessage());
}
try {
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
index f2165f8..1a32b7f 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/svcs/DefaultSubscriptionInternalApi.java
@@ -224,6 +224,8 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
}
boolean first = true;
+ final List<SubscriptionBase> subscriptionsForBundle = getSubscriptionsForBundle(bundleId, null, context);
+
for (EntitlementSpecifier entitlement : entitlements) {
final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
@@ -247,7 +249,7 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
// verify the number of subscriptions (of the same kind) allowed per bundle and the existing ones
if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
if (plan.getPlansAllowedInBundle() != -1 && plan.getPlansAllowedInBundle() > 0) {
- int existingAddOnsWithSamePlanName = countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(bundleId, null, context), plan.getName());
+ int existingAddOnsWithSamePlanName = countExistingAddOnsWithSamePlanName(subscriptionsForBundle, plan.getName());
int currentAddOnsWithSamePlanName = countCurrentAddOnsWithSamePlanName(entitlements, catalog, plan.getName(), effectiveDate, callContext);
if ((existingAddOnsWithSamePlanName + currentAddOnsWithSamePlanName) > plan.getPlansAllowedInBundle()) {
// a new ADD_ON subscription of the same plan can't be added because it has reached its limit by bundle
@@ -518,8 +520,26 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
final PlanSpecifier spec,
final DateTime requestedDateWithMs,
final BillingActionPolicy requestedPolicy,
- final InternalTenantContext context) throws SubscriptionBaseApiException {
+ final List<PlanPhasePriceOverride> overrides,
+ 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 = catalogService.getFullCatalog(true, true, context);
+ final DateTime now = clock.getUTCNow();
+ final DateTime effectiveDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+ final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(overrides, callContext);
+ final Plan plan = catalog.createOrFindPlan(spec, overridesWithContext, effectiveDate);
+ if (ProductCategory.ADD_ON.toString().equalsIgnoreCase(plan.getProduct().getCategory().toString())) {
+ if (plan.getPlansAllowedInBundle() != -1
+ && plan.getPlansAllowedInBundle() > 0
+ && countExistingAddOnsWithSamePlanName(getSubscriptionsForBundle(subscription.getBundleId(), null, 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, requestedDateWithMs, requestedPolicy, tenantContext);
}