killbill-aplcache

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 b42b21f..5679190 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
@@ -28,6 +28,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
@@ -40,6 +41,9 @@ public interface SubscriptionBaseInternalApi {
     public SubscriptionBase createSubscription(UUID bundleId, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, DateTime requestedDateWithMs,
                                                InternalCallContext context) throws SubscriptionBaseApiException;
 
+    public SubscriptionBase createBaseSubscriptionWithAddOns(UUID bundleId, Iterable<EntitlementSpecifier> entitlements, DateTime requestedDateWithMs,
+                                                             InternalCallContext context) throws SubscriptionBaseApiException;
+
     public SubscriptionBaseBundle createBundleForAccount(UUID accountId, String bundleName, InternalCallContext context)
             throws SubscriptionBaseApiException;
 
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
index cab5264..4f039af 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementApi.java
@@ -31,6 +31,7 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.AccountEventsStreams;
 import org.killbill.billing.entitlement.EntitlementService;
 import org.killbill.billing.entitlement.EventsStream;
@@ -156,6 +157,58 @@ public class DefaultEntitlementApi extends DefaultEntitlementApiBase implements 
     }
 
     @Override
+    public Entitlement createBaseEntitlementWithAddOns(final UUID accountId, final Iterable<EntitlementSpecifier> entitlementSpecifier, final LocalDate effectiveDate,
+                                                             final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
+
+        final EntitlementSpecifier baseSpecifier = Iterables.tryFind(entitlementSpecifier, new Predicate<EntitlementSpecifier>() {
+            @Override
+            public boolean apply(final EntitlementSpecifier specifier) {
+                return specifier.getPlanPhaseSpecifier() != null && ProductCategory.BASE.equals(specifier.getPlanPhaseSpecifier().getProductCategory());
+            }
+        }).orNull();
+
+        if (baseSpecifier == null) {
+            throw new EntitlementApiException(new IllegalArgumentException(), ErrorCode.SUB_CREATE_NO_BP.getCode(), "Missing Base Subscription.");
+        }
+
+        final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
+                                                                               accountId,
+                                                                               null,
+                                                                               null,
+                                                                               baseSpecifier.getPlanPhaseSpecifier(),
+                                                                               baseSpecifier.getExternalkey(),
+                                                                               baseSpecifier.getOverrides(),
+                                                                               effectiveDate,
+                                                                               properties,
+                                                                               callContext);
+
+        final WithEntitlementPlugin<Entitlement> createBaseEntitlementWithAddOn = new WithEntitlementPlugin<Entitlement>() {
+            @Override
+            public Entitlement doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {
+                final InternalCallContext contextWithValidAccountRecordId = internalCallContextFactory.createInternalCallContext(accountId, callContext);
+
+                try {
+                    final SubscriptionBaseBundle bundle = subscriptionBaseInternalApi.createBundleForAccount(accountId, baseSpecifier.getExternalkey(), contextWithValidAccountRecordId);
+
+                    final DateTime referenceTime = clock.getUTCNow();
+                    final DateTime requestedDate = dateHelper.fromLocalDateAndReferenceTime(updatedPluginContext.getEffectiveDate(), referenceTime, contextWithValidAccountRecordId);
+                    final SubscriptionBase subscription = subscriptionBaseInternalApi.createBaseSubscriptionWithAddOns(bundle.getId(), entitlementSpecifier, requestedDate, contextWithValidAccountRecordId);
+
+                    return new DefaultEntitlement(subscription.getId(), eventsStreamBuilder, entitlementApi, pluginExecution,
+                                                  blockingStateDao, subscriptionBaseInternalApi, checker, notificationQueueService,
+                                                  entitlementUtils, dateHelper, clock, securityApi, internalCallContextFactory, callContext);
+
+
+                } catch (SubscriptionBaseApiException e) {
+                    throw new EntitlementApiException(e);
+                }
+
+            }
+        };
+        return pluginExecution.executeWithPlugin(createBaseEntitlementWithAddOn, pluginContext);
+    }
+
+    @Override
     public Entitlement addEntitlement(final UUID bundleId, final PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides, final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {
 
         final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.CREATE_SUBSCRIPTION,
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
new file mode 100644
index 0000000..81c12ce
--- /dev/null
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/api/DefaultEntitlementSpecifier.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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.entitlement.api;
+
+import java.util.List;
+
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
+import org.killbill.billing.catalog.api.PlanSpecifier;
+
+public class DefaultEntitlementSpecifier implements EntitlementSpecifier {
+
+    private String externalkey;
+    private PlanPhaseSpecifier PlanPhaseSpecifier;
+    private List<PlanPhasePriceOverride> overrides;
+
+    public DefaultEntitlementSpecifier(final String externalkey, final org.killbill.billing.catalog.api.PlanPhaseSpecifier planPhaseSpecifier, final List<PlanPhasePriceOverride> overrides) {
+        this.externalkey = externalkey;
+        PlanPhaseSpecifier = planPhaseSpecifier;
+        this.overrides = overrides;
+    }
+
+    @Override
+    public String getExternalkey() {
+        return externalkey;
+    }
+
+    @Override
+    public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+        return PlanPhaseSpecifier;
+    }
+
+    @Override
+    public List<PlanPhasePriceOverride> getOverrides() {
+        return overrides;
+    }
+
+}
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
index a60129b..f899e04 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlementApi.java
@@ -16,6 +16,7 @@
 
 package org.killbill.billing.entitlement.api;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
@@ -42,7 +43,10 @@ import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import com.google.common.collect.ImmutableList;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedDB {
 
@@ -485,4 +489,106 @@ public class TestDefaultEntitlementApi extends EntitlementTestSuiteWithEmbeddedD
 
     }
 
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithAddOns() throws AccountApiException, EntitlementApiException, SubscriptionBaseApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Cleaning", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier("baseExternalKey", baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier = new DefaultEntitlementSpecifier("addOnExternalKey", addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier);
+
+        // Keep the same object for the whole test, to make sure we refresh its state before r/w calls
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.CREATE);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlementWithAddOns(account.getId(), specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        assertNotNull(entitlement);
+
+        final List<Entitlement> allEntitlementsForBundle = entitlementApi.getAllEntitlementsForBundle(entitlement.getBundleId(), callContext);
+        assertTrue(allEntitlementsForBundle.size() == 2);
+
+        final Entitlement baseEntitlement = allEntitlementsForBundle.get(0);
+        final Entitlement addOnEntitlement = allEntitlementsForBundle.get(1);
+
+        assertEquals(baseEntitlement.getLastActiveProduct().getName(), "Pistol");
+        assertEquals(baseEntitlement.getLastActiveProductCategory(), ProductCategory.BASE);
+
+        assertEquals(addOnEntitlement.getLastActiveProduct().getName(), "Cleaning");
+        assertEquals(addOnEntitlement.getLastActiveProductCategory(), ProductCategory.ADD_ON);
+
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithInvalidAddOn() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Pistol", ProductCategory.BASE, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Invalid", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        EntitlementSpecifier baseEntitlementSpecifier = new DefaultEntitlementSpecifier("baseExternalKey", baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier = new DefaultEntitlementSpecifier("addOnExternalKey", addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(baseEntitlementSpecifier);
+        specifierList.add(addOnEntitlementSpecifier);
+
+        // Keep the same object for the whole test, to make sure we refresh its state before r/w calls
+        testListener.pushExpectedEvents();
+        try {
+            entitlementApi.createBaseEntitlementWithAddOns(account.getId(), specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+            fail();
+        } catch (EntitlementApiException e) {
+            assertEquals(e.getMessage(), "Could not find any product named 'Invalid'");
+        }
+        assertListenerStatus();
+
+        final List<Entitlement> allEntitlementsForAccount = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        assertTrue(allEntitlementsForAccount.size() == 0);
+
+    }
+
+    @Test(groups = "slow")
+    public void testCreateBaseEntitlementWithoutBaseEntitlement() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = accountApi.createAccount(getAccountData(7), callContext);
+
+        final PlanPhaseSpecifier baseSpec = new PlanPhaseSpecifier("Cleaning", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+        final PlanPhaseSpecifier addOnSpec = new PlanPhaseSpecifier("Bullets", ProductCategory.ADD_ON, BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null);
+
+        EntitlementSpecifier addOnEntitlementSpecifier1 = new DefaultEntitlementSpecifier("addOnExternalKey", baseSpec, null);
+        EntitlementSpecifier addOnEntitlementSpecifier2 = new DefaultEntitlementSpecifier("addOnExternalKey2", addOnSpec, null);
+
+        final List<EntitlementSpecifier> specifierList = new ArrayList<EntitlementSpecifier>();
+        specifierList.add(addOnEntitlementSpecifier1);
+        specifierList.add(addOnEntitlementSpecifier2);
+
+        // Keep the same object for the whole test, to make sure we refresh its state before r/w calls
+        testListener.pushExpectedEvents();
+        try {
+            entitlementApi.createBaseEntitlementWithAddOns(account.getId(), specifierList, initialDate, ImmutableList.<PluginProperty>of(), callContext);
+            fail();
+        } catch (EntitlementApiException e) {
+            assertEquals(e.getMessage(), "Missing Base Subscription.");
+        }
+        assertListenerStatus();
+
+        final List<Entitlement> allEntitlementsForAccount = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
+        assertTrue(allEntitlementsForAccount.size() == 0);
+
+    }
+
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 28cd1aa..6292294 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -470,4 +470,9 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
             Preconditions.checkArgument(expression, errorMessage);
         }
     }
+
+    protected void verifyNumberOfElements(int actual, int expected, String errorMessage) {
+        Preconditions.checkArgument(actual == expected, errorMessage);
+    }
+
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
index 7e4dc8d..3862996 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/SubscriptionResource.java
@@ -19,6 +19,7 @@
 package org.killbill.billing.jaxrs.resources;
 
 import java.math.BigDecimal;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.TimeoutException;
@@ -54,6 +55,7 @@ import org.killbill.billing.entitlement.api.Entitlement;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.entitlement.api.EntitlementApi;
 import org.killbill.billing.entitlement.api.EntitlementApiException;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.entitlement.api.Subscription;
 import org.killbill.billing.entitlement.api.SubscriptionApi;
 import org.killbill.billing.entitlement.api.SubscriptionApiException;
@@ -87,7 +89,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.codahale.metrics.annotation.Timed;
-import com.google.common.collect.ImmutableList;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 import com.wordnik.swagger.annotations.Api;
 import com.wordnik.swagger.annotations.ApiOperation;
@@ -208,7 +212,122 @@ public class SubscriptionResource extends JaxRsResourceBase {
         return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
     }
 
+    @Timed
+    @POST
+    @Path("/createEntitlementWithAddOns")
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Create an entitlement with addOn products")
+    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid entitlement supplied")})
+    public Response createEntitlementWithAddOns(final List<SubscriptionJson> entitlements,
+                                      @QueryParam(QUERY_REQUESTED_DT) final String requestedDate,
+                                      @QueryParam(QUERY_CALL_COMPLETION) @DefaultValue("false") final Boolean callCompletion,
+                                      @QueryParam(QUERY_CALL_TIMEOUT) @DefaultValue("3") final long timeoutSec,
+                                      @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                      @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                      @HeaderParam(HDR_REASON) final String reason,
+                                      @HeaderParam(HDR_COMMENT) final String comment,
+                                      @javax.ws.rs.core.Context final HttpServletRequest request,
+                                      @javax.ws.rs.core.Context final UriInfo uriInfo) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
+
+        Preconditions.checkArgument(Iterables.size(entitlements) > 0, "Subscription list mustn't be null or empty.");
+
+        for (SubscriptionJson entitlement : entitlements) {
+            verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified for each element");
+            verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set for each element",
+                                 entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set for each element",
+                                 entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set for each element",
+                                 entitlement.getPriceList(), "SubscriptionJson priceList needs to be set for each element");
+        }
+
+        final int baseSubscriptionsSize = Iterables.size(Iterables.filter(entitlements, new Predicate<SubscriptionJson>() {
+            @Override
+            public boolean apply(final SubscriptionJson subscription) {
+                return subscription.getProductCategory().equals(ProductCategory.BASE.toString());
+            }
+        }));
+        verifyNumberOfElements(baseSubscriptionsSize, 1, "Only one BASE product is allowed.");
+
+        final int addOnSubscriptionsSize = Iterables.size(Iterables.filter(entitlements, new Predicate<SubscriptionJson>() {
+            @Override
+            public boolean apply(final SubscriptionJson subscription) {
+                return subscription.getProductCategory().equals(ProductCategory.ADD_ON.toString());
+            }
+        }));
+        verifyNumberOfElements(addOnSubscriptionsSize, entitlements.size() - 1, "It should be " + (entitlements.size() - 1) + " ADD_ON products.");
+
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        final SubscriptionJson baseEntitlement = Iterables.tryFind(entitlements, new Predicate<SubscriptionJson>() {
+                    @Override
+                    public boolean apply(final SubscriptionJson subscription) {
+                        return ProductCategory.BASE.toString().equalsIgnoreCase(subscription.getProductCategory());
+                    }
+            }).orNull();
+
+        verifyNonNull(baseEntitlement.getAccountId(), "SubscriptionJson accountId needs to be set for BASE product.");
+
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        final EntitlementCallCompletionCallback<Entitlement> callback = new EntitlementCallCompletionCallback<Entitlement>() {
 
+            @Override
+            public Entitlement doOperation(final CallContext ctx) throws InterruptedException, TimeoutException, EntitlementApiException, SubscriptionApiException, AccountApiException {
+
+                List<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
+                final Account account = getAccountFromSubscriptionJson(baseEntitlement, callContext);
+
+                for (final SubscriptionJson entitlement : entitlements) {
+
+                    final PlanPhaseSpecifier planPhaseSpecifier = new PlanPhaseSpecifier(entitlement.getProductName(),
+                                                                                         ProductCategory.valueOf(entitlement.getProductCategory()),
+                                                                                         BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList(), null);
+
+                    final PlanSpecifier planSpec = new PlanSpecifier(entitlement.getProductName(),
+                                                                    ProductCategory.valueOf(entitlement.getProductCategory()),
+                                                                    BillingPeriod.valueOf(entitlement.getBillingPeriod()), entitlement.getPriceList());
+                    final List<PlanPhasePriceOverride> overrides = PhasePriceOverrideJson.toPlanPhasePriceOverrides(entitlement.getPriceOverrides(), planSpec, account.getCurrency());
+
+                    EntitlementSpecifier specifier = new EntitlementSpecifier() {
+
+                        @Override
+                        public String getExternalkey() {
+                            return entitlement.getExternalKey();
+                        }
+
+                        @Override
+                        public PlanPhaseSpecifier getPlanPhaseSpecifier() {
+                            return planPhaseSpecifier;
+                        }
+
+                        @Override
+                        public List<PlanPhasePriceOverride> getOverrides() {
+                            return overrides;
+                        }
+                    };
+
+                    entitlementSpecifierList.add(specifier);
+                }
+
+                final LocalDate inputLocalDate = toLocalDate(account, requestedDate, callContext);
+                return entitlementApi.createBaseEntitlementWithAddOns(account.getId(), entitlementSpecifierList, inputLocalDate,
+                                                                      pluginProperties, callContext);
+            }
+
+            @Override
+            public boolean isImmOperation() {
+                return true;
+            }
+
+            @Override
+            public Response doResponseOk(final Entitlement entitlement) {
+                return uriBuilder.buildResponse(uriInfo, BundleResource.class, "getBundle", entitlement.getBundleId());
+            }
+
+        };
+
+        final EntitlementCallCompletion<Entitlement> callCompletionCreation = new EntitlementCallCompletion<Entitlement>();
+        return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
+    }
 
     @Timed
     @PUT

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index b508bfa..a37a070 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.57</version>
+        <version>0.58-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.15.8-SNAPSHOT</version>
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
index a96cac4..0a9e54f 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/SubscriptionBaseApiService.java
@@ -35,6 +35,7 @@ import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.subscription.api.user.DefaultSubscriptionBase;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseApiException;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.SubscriptionEspecifier;
 import org.killbill.billing.subscription.events.SubscriptionBaseEvent;
 import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.TenantContext;
@@ -46,6 +47,9 @@ public interface SubscriptionBaseApiService {
                                               CallContext context)
             throws SubscriptionBaseApiException;
 
+    public DefaultSubscriptionBase createPlans(Iterable<SubscriptionEspecifier> subscriptions, CallContext context)
+            throws SubscriptionBaseApiException;
+
     @Deprecated
     public boolean recreatePlan(DefaultSubscriptionBase subscription, PlanPhaseSpecifier spec, List<PlanPhasePriceOverride> overrides, DateTime requestedDateWithMs, CallContext context)
             throws SubscriptionBaseApiException;
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 33a57d3..3267148 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
@@ -46,6 +46,7 @@ import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun;
 import org.killbill.billing.entitlement.api.EntitlementAOStatusDryRun.DryRunChangeReason;
+import org.killbill.billing.entitlement.api.EntitlementSpecifier;
 import org.killbill.billing.events.EffectiveSubscriptionInternalEvent;
 import org.killbill.billing.invoice.api.DryRunArguments;
 import org.killbill.billing.subscription.api.SubscriptionApiBase;
@@ -62,6 +63,7 @@ import org.killbill.billing.subscription.api.user.SubscriptionBaseBundle;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransition;
 import org.killbill.billing.subscription.api.user.SubscriptionBaseTransitionData;
 import org.killbill.billing.subscription.api.user.SubscriptionBuilder;
+import org.killbill.billing.subscription.api.user.SubscriptionEspecifier;
 import org.killbill.billing.subscription.engine.addon.AddonUtils;
 import org.killbill.billing.subscription.engine.core.DefaultSubscriptionBaseService;
 import org.killbill.billing.subscription.engine.dao.SubscriptionDao;
@@ -162,6 +164,59 @@ public class DefaultSubscriptionInternalApi extends SubscriptionApiBase implemen
     }
 
     @Override
+    public SubscriptionBase createBaseSubscriptionWithAddOns(final UUID bundleId, final Iterable<EntitlementSpecifier> entitlements, final DateTime requestedDateWithMs, final InternalCallContext context) throws SubscriptionBaseApiException {
+
+        final DateTime now = clock.getUTCNow();
+        final DateTime requestedDate = (requestedDateWithMs != null) ? DefaultClock.truncateMs(requestedDateWithMs) : now;
+
+        try {
+            final List<SubscriptionEspecifier> subscriptions = new ArrayList<SubscriptionEspecifier>();
+            final Catalog catalog = catalogService.getFullCatalog(context);
+            final CallContext callContext = internalCallContextFactory.createCallContext(context);
+
+            for (EntitlementSpecifier entitlement : entitlements) {
+
+                final PlanPhaseSpecifier spec = entitlement.getPlanPhaseSpecifier();
+                final String realPriceList = (spec.getPriceListName() == null) ? PriceListSet.DEFAULT_PRICELIST_NAME : spec.getPriceListName();
+
+                final PlanPhasePriceOverridesWithCallContext overridesWithContext = new DefaultPlanPhasePriceOverridesWithCallContext(entitlement.getOverrides(), callContext);
+
+                final Plan plan = catalog.createOrFindPlan(spec.getProductName(), spec.getBillingPeriod(), realPriceList, overridesWithContext, requestedDate);
+                final PlanPhase phase = plan.getAllPhases()[0];
+                if (phase == null) {
+                    throw new SubscriptionBaseError(String.format("No initial PlanPhase for Product %s, term %s and set %s does not exist in the catalog",
+                                                                  spec.getProductName(), spec.getBillingPeriod().toString(), realPriceList));
+                }
+
+                final SubscriptionBaseBundle bundle = dao.getSubscriptionBundleFromId(bundleId, context);
+                if (bundle == null) {
+                    throw new SubscriptionBaseApiException(ErrorCode.SUB_CREATE_NO_BUNDLE, bundleId);
+                }
+
+                SubscriptionEspecifier subscription = new SubscriptionEspecifier();
+                subscription.setRealPriceList(realPriceList);
+                subscription.setRequestedDate(requestedDate);
+                subscription.setEffectiveDate(requestedDate);
+                subscription.setProcessedDate(now);
+                subscription.setPlan(plan);
+                subscription.setInitialPhase(spec.getPhaseType());
+                subscription.setBuilder(new SubscriptionBuilder()
+                                                .setId(UUIDs.randomUUID())
+                                                .setBundleId(bundleId)
+                                                .setCategory(plan.getProduct().getCategory())
+                                                .setBundleStartDate(requestedDate)
+                                                .setAlignStartDate(requestedDate));
+
+                subscriptions.add(subscription);
+            }
+
+            return apiService.createPlans(subscriptions, callContext);
+        } catch (final CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+    }
+
+    @Override
     public SubscriptionBaseBundle createBundleForAccount(final UUID accountId, final String bundleKey, final InternalCallContext context) throws SubscriptionBaseApiException {
 
         final List<SubscriptionBaseBundle> existingBundles = dao.getSubscriptionBundlesForKey(bundleKey, context);
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
index 2e829f3..6769993 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/DefaultSubscriptionBaseApiService.java
@@ -19,8 +19,10 @@
 package org.killbill.billing.subscription.api.user;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
@@ -46,6 +48,7 @@ import org.killbill.billing.catalog.api.PriceListSet;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementState;
+import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.subscription.alignment.PlanAligner;
 import org.killbill.billing.subscription.alignment.TimedPhase;
 import org.killbill.billing.subscription.api.SubscriptionBase;
@@ -70,7 +73,9 @@ import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.clock.Clock;
 import org.killbill.clock.DefaultClock;
 
+import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.inject.Inject;
 
 public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiService {
@@ -104,6 +109,52 @@ public class DefaultSubscriptionBaseApiService implements SubscriptionBaseApiSer
         return subscription;
     }
 
+    @Override
+    public DefaultSubscriptionBase createPlans(final Iterable<SubscriptionEspecifier> subscriptions, final CallContext context) throws SubscriptionBaseApiException {
+
+        Map<UUID, List<SubscriptionBaseEvent>> eventsMap = new HashMap<UUID, List<SubscriptionBaseEvent>>();
+        List<DefaultSubscriptionBase> subscriptionBaseList = new ArrayList<DefaultSubscriptionBase>();
+        for (SubscriptionEspecifier subscription : subscriptions) {
+
+            try {
+                final DefaultSubscriptionBase subscriptionBase = new DefaultSubscriptionBase(subscription.getBuilder(), this, clock);
+                final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBase.getBundleId(), context);
+                final List<SubscriptionBaseEvent> events = getEventsOnCreation(subscriptionBase.getBundleId(), subscriptionBase.getId(), subscriptionBase.getAlignStartDate(),
+                                                                               subscriptionBase.getBundleStartDate(), subscriptionBase.getActiveVersion(), subscription.getPlan(),
+                                                                               subscription.getInitialPhase(), subscription.getRealPriceList(), subscription.getRequestedDate(),
+                                                                               subscription.getEffectiveDate(), subscription.getProcessedDate(), false, internalCallContext);
+
+                eventsMap.put(subscriptionBase.getId(), events);
+                subscriptionBaseList.add(subscriptionBase);
+
+            } catch (final CatalogApiException e) {
+                throw new SubscriptionBaseApiException(e);
+            }
+        }
+
+        final InternalCallContext internalCallContext = createCallContextFromBundleId(subscriptionBaseList.get(0).getBundleId(), context);
+        dao.createSubscriptionWithAddOns(subscriptionBaseList, eventsMap, internalCallContext);
+
+        try {
+            for (DefaultSubscriptionBase subscriptionBase : subscriptionBaseList) {
+                subscriptionBase.rebuildTransitions(dao.getEventsForSubscription(subscriptionBase.getId(), internalCallContext), catalogService.getFullCatalog(internalCallContext));
+            }
+        } catch (CatalogApiException e) {
+            throw new SubscriptionBaseApiException(e);
+        }
+
+        return findBaseSubscription(subscriptionBaseList);
+    }
+
+    private DefaultSubscriptionBase findBaseSubscription(final List<DefaultSubscriptionBase> subscriptionBaseList) {
+        return Iterables.tryFind(subscriptionBaseList, new Predicate<DefaultSubscriptionBase>() {
+            @Override
+            public boolean apply(final DefaultSubscriptionBase subscription) {
+                return ProductCategory.BASE.equals(subscription.getCategory());
+            }
+        }).orNull();
+    }
+
     @Deprecated
     @Override
     public boolean recreatePlan(final DefaultSubscriptionBase subscription, final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDateWithMs, final CallContext context)
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEspecifier.java b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEspecifier.java
new file mode 100644
index 0000000..2a6fa9a
--- /dev/null
+++ b/subscription/src/main/java/org/killbill/billing/subscription/api/user/SubscriptionEspecifier.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 The Billing Project, LLC
+ *
+ * The Billing Project 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.user;
+
+import org.joda.time.DateTime;
+import org.killbill.billing.catalog.api.PhaseType;
+import org.killbill.billing.catalog.api.Plan;
+
+public class SubscriptionEspecifier {
+
+    private SubscriptionBuilder builder;
+    private Plan plan;
+    private PhaseType initialPhase;
+    private String realPriceList;
+    private DateTime requestedDate;
+    private DateTime effectiveDate;
+    private DateTime processedDate;
+
+    public SubscriptionEspecifier() {
+    }
+
+    public SubscriptionEspecifier(final SubscriptionBuilder builder, final Plan plan,
+                                  final PhaseType initialPhase, final String realPriceList,
+                                  final DateTime requestedDate, final DateTime effectiveDate,
+                                  final DateTime processedDate) {
+        this.builder = builder;
+        this.plan = plan;
+        this.initialPhase = initialPhase;
+        this.realPriceList = realPriceList;
+        this.requestedDate = requestedDate;
+        this.effectiveDate = effectiveDate;
+        this.processedDate = processedDate;
+    }
+
+    public SubscriptionBuilder getBuilder() {
+        return builder;
+    }
+
+    public void setBuilder(final SubscriptionBuilder builder) {
+        this.builder = builder;
+    }
+
+    public Plan getPlan() {
+        return plan;
+    }
+
+    public void setPlan(final Plan plan) {
+        this.plan = plan;
+    }
+
+    public PhaseType getInitialPhase() {
+        return initialPhase;
+    }
+
+    public void setInitialPhase(final PhaseType initialPhase) {
+        this.initialPhase = initialPhase;
+    }
+
+    public String getRealPriceList() {
+        return realPriceList;
+    }
+
+    public void setRealPriceList(final String realPriceList) {
+        this.realPriceList = realPriceList;
+    }
+
+    public DateTime getRequestedDate() {
+        return requestedDate;
+    }
+
+    public void setRequestedDate(final DateTime requestedDate) {
+        this.requestedDate = requestedDate;
+    }
+
+    public DateTime getEffectiveDate() {
+        return effectiveDate;
+    }
+
+    public void setEffectiveDate(final DateTime effectiveDate) {
+        this.effectiveDate = effectiveDate;
+    }
+
+    public DateTime getProcessedDate() {
+        return processedDate;
+    }
+
+    public void setProcessedDate(final DateTime processedDate) {
+        this.processedDate = processedDate;
+    }
+
+}
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
index 63d5d56..4a72049 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/DefaultSubscriptionDao.java
@@ -510,6 +510,35 @@ public class DefaultSubscriptionDao extends EntityDaoBase<SubscriptionBundleMode
     }
 
     @Override
+    public void createSubscriptionWithAddOns(final List<DefaultSubscriptionBase> subscriptions, final Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap, final InternalCallContext context) {
+        transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
+            @Override
+            public Void inTransaction(final EntitySqlDaoWrapperFactory entitySqlDaoWrapperFactory) throws Exception {
+                final SubscriptionSqlDao transactional = entitySqlDaoWrapperFactory.become(SubscriptionSqlDao.class);
+                final SubscriptionEventSqlDao eventsDaoFromSameTransaction = entitySqlDaoWrapperFactory.become(SubscriptionEventSqlDao.class);
+
+                for (DefaultSubscriptionBase subscription : subscriptions) {
+                    transactional.create(new SubscriptionModelDao(subscription), context);
+
+                    final List<SubscriptionBaseEvent> initialEvents = initialEventsMap.get(subscription.getId());
+                    for (final SubscriptionBaseEvent cur : initialEvents) {
+                        eventsDaoFromSameTransaction.create(new SubscriptionEventModelDao(cur), context);
+
+                        final boolean isBusEvent = cur.getEffectiveDate().compareTo(clock.getUTCNow()) <= 0 && (cur.getType() == EventType.API_USER);
+                        recordBusOrFutureNotificationFromTransaction(subscription, cur, entitySqlDaoWrapperFactory, isBusEvent, 0, context);
+
+                    }
+                    // Notify the Bus of the latest requested change, if needed
+                    if (initialEvents.size() > 0) {
+                        notifyBusOfRequestedChange(entitySqlDaoWrapperFactory, subscription, initialEvents.get(initialEvents.size() - 1), SubscriptionBaseTransitionType.CREATE, context);
+                    }
+                }
+                return null;
+            }
+        });
+    }
+
+    @Override
     public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @Override
diff --git a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
index 13cfa2b..893c5d4 100644
--- a/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
+++ b/subscription/src/main/java/org/killbill/billing/subscription/engine/dao/SubscriptionDao.java
@@ -82,6 +82,8 @@ public interface SubscriptionDao extends EntityDao<SubscriptionBundleModelDao, S
     // SubscriptionBase creation, cancellation, changePlanWithRequestedDate apis
     public void createSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> initialEvents, InternalCallContext context);
 
+    public void createSubscriptionWithAddOns(List<DefaultSubscriptionBase> subscriptions, Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap, InternalCallContext context);
+
     public void recreateSubscription(DefaultSubscriptionBase subscription, List<SubscriptionBaseEvent> recreateEvents, InternalCallContext context);
 
     public void cancelSubscription(DefaultSubscriptionBase subscription, SubscriptionBaseEvent cancelEvent, InternalCallContext context, int cancelSeq);
diff --git a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
index 1d95133..6aaed33 100644
--- a/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
+++ b/subscription/src/test/java/org/killbill/billing/subscription/engine/dao/MockSubscriptionDaoMemory.java
@@ -213,6 +213,24 @@ public class MockSubscriptionDaoMemory extends MockEntityDaoBase<SubscriptionBun
     }
 
     @Override
+    public void createSubscriptionWithAddOns(final List<DefaultSubscriptionBase> subscriptions,
+                                             final Map<UUID, List<SubscriptionBaseEvent>> initialEventsMap,
+                                             final InternalCallContext context) {
+        synchronized (events) {
+            for (DefaultSubscriptionBase subscription : subscriptions) {
+                final List<SubscriptionBaseEvent> initialEvents = initialEventsMap.get(subscription.getId());
+                events.addAll(initialEvents);
+                for (final SubscriptionBaseEvent cur : initialEvents) {
+                    recordFutureNotificationFromTransaction(null, cur.getEffectiveDate(), new SubscriptionNotificationKey(cur.getId()), context);
+                }
+                final SubscriptionBase updatedSubscription = buildSubscription(subscription, context);
+                this.subscriptions.add(updatedSubscription);
+                mockNonEntityDao.addTenantRecordIdMapping(updatedSubscription.getId(), context);
+            }
+        }
+    }
+
+    @Override
     public void recreateSubscription(final DefaultSubscriptionBase subscription, final List<SubscriptionBaseEvent> recreateEvents, final InternalCallContext context) {
         synchronized (events) {
             events.addAll(recreateEvents);