use of org.killbill.billing.entitlement.api.Entitlement in project killbill by killbill.
the class TestDefaultInternalBillingApi method testBCDUpdateMultipleSubscriptionsAccountAligned.
@Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/865")
public void testBCDUpdateMultipleSubscriptionsAccountAligned() throws Exception {
final LocalDate initialDate = new LocalDate(2013, 8, 7);
clock.setDay(initialDate);
// Account with no BCD
final Account account = createAccount(getAccountData(0));
Assert.assertEquals(account.getBillCycleDayLocal(), (Integer) 0);
// Create 2 entitlements, one base with one add-on. All entitlements are ACCOUNT aligned
final String bundleKey1 = UUID.randomUUID().toString();
final EntitlementSpecifier entitlementSpecifierBase1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Shotgun", BillingPeriod.MONTHLY, PriceListSet.DEFAULT_PRICELIST_NAME, null));
final EntitlementSpecifier entitlementSpecifierAO1 = new DefaultEntitlementSpecifier(new PlanPhaseSpecifier("Cabinet", BillingPeriod.TRIANNUAL, PriceListSet.DEFAULT_PRICELIST_NAME, null));
final BaseEntitlementWithAddOnsSpecifier specifier1 = new DefaultBaseEntitlementWithAddOnsSpecifier(null, bundleKey1, ImmutableList.of(entitlementSpecifierBase1, entitlementSpecifierAO1), null, null, false);
testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK, NextEvent.CREATE, NextEvent.BLOCK);
entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), ImmutableList.of(specifier1), false, ImmutableList.<PluginProperty>of(), callContext);
assertListenerStatus();
final List<Entitlement> entitlements = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
Assert.assertEquals(entitlements.size(), 2);
Assert.assertEquals(entitlements.get(0).getLastActiveProduct().getName(), "Shotgun");
// See bug description at https://github.com/killbill/killbill/issues/865 and PR discussion at https://github.com/killbill/killbill/pull/1067/files/926ca68c32c8f8c93bd5eda94fba17f6e19e593d#r237981095
Assert.assertNull(entitlements.get(0).getBillCycleDayLocal());
Assert.assertEquals(entitlements.get(1).getLastActiveProduct().getName(), "Cabinet");
Assert.assertNull(entitlements.get(1).getBillCycleDayLocal());
// Account still has no BCD
final Account accountNoBCD = accountApi.getAccountById(account.getId(), callContext);
Assert.assertEquals(accountNoBCD.getBillCycleDayLocal(), (Integer) 0);
List<BillingEvent> events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, null, internalCallContext));
Assert.assertEquals(events.size(), 3);
for (final BillingEvent billingEvent : events) {
Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 7);
}
// Verify BCD
final Account accountWithBCD = accountApi.getAccountById(account.getId(), callContext);
Assert.assertEquals(accountWithBCD.getBillCycleDayLocal(), (Integer) 7);
// Verify GET
final List<Entitlement> entitlementsUpdated = entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext);
Assert.assertEquals(entitlementsUpdated.size(), 2);
Assert.assertEquals(entitlementsUpdated.get(0).getLastActiveProduct().getName(), "Shotgun");
Assert.assertEquals(entitlementsUpdated.get(0).getBillCycleDayLocal(), (Integer) 7);
Assert.assertEquals(entitlementsUpdated.get(1).getLastActiveProduct().getName(), "Cabinet");
Assert.assertEquals(entitlementsUpdated.get(1).getBillCycleDayLocal(), (Integer) 7);
events = ImmutableList.<BillingEvent>copyOf(billingInternalApi.getBillingEventsForAccountAndUpdateAccountBCD(account.getId(), null, null, internalCallContext));
Assert.assertEquals(events.size(), 3);
for (final BillingEvent billingEvent : events) {
Assert.assertEquals(billingEvent.getBillCycleDayLocal(), 7);
}
}
use of org.killbill.billing.entitlement.api.Entitlement in project killbill by killbill.
the class SubscriptionResource method changeSubscriptionPlan.
@TimedResource
@PUT
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
@Path("/{subscriptionId:" + UUID_PATTERN + "}")
@ApiOperation(value = "Change entitlement plan")
@ApiResponses(value = { @ApiResponse(code = 204, message = "Successful operation"), @ApiResponse(code = 400, message = "Invalid subscription id supplied"), @ApiResponse(code = 404, message = "Entitlement not found") })
public Response changeSubscriptionPlan(@PathParam("subscriptionId") final UUID subscriptionId, final SubscriptionJson entitlement, @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_BILLING_POLICY) final BillingActionPolicy billingPolicy, @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) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified");
if (entitlement.getPlanName() == null) {
verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set", entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set", entitlement.getPriceList(), "SubscriptionJson priceList needs to be set");
}
Preconditions.checkArgument(requestedDate == null || billingPolicy == null, "Only one of requestedDate or billingPolicy should be specified");
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContextNoAccountId = context.createCallContextNoAccountId(createdBy, reason, comment, request);
final Entitlement current = entitlementApi.getEntitlementForId(subscriptionId, callContextNoAccountId);
final CallContext callContext = context.createCallContextWithAccountId(current.getAccountId(), createdBy, reason, comment, request);
final EntitlementCallCompletionCallback<Response> callback = new EntitlementCallCompletionCallback<Response>() {
private boolean isImmediateOp = true;
@Override
public Response doOperation(final CallContext ctx) throws EntitlementApiException, AccountApiException {
final LocalDate inputLocalDate = toLocalDate(requestedDate);
final Entitlement newEntitlement;
final Account account = accountUserApi.getAccountById(current.getAccountId(), callContext);
final EntitlementSpecifier spec = buildEntitlementSpecifier(entitlement, account.getCurrency(), entitlement.getExternalKey());
if (requestedDate == null && billingPolicy == null) {
newEntitlement = current.changePlan(spec, pluginProperties, ctx);
} else if (billingPolicy == null) {
newEntitlement = current.changePlanWithDate(spec, inputLocalDate, pluginProperties, ctx);
} else {
newEntitlement = current.changePlanOverrideBillingPolicy(spec, null, billingPolicy, pluginProperties, ctx);
}
isImmediateOp = newEntitlement.getLastActiveProduct().getName().equals(entitlement.getProductName()) && newEntitlement.getLastActivePlan().getRecurringBillingPeriod() == entitlement.getBillingPeriod() && newEntitlement.getLastActivePriceList().getName().equals(entitlement.getPriceList());
return Response.status(Status.NO_CONTENT).build();
}
@Override
public boolean isImmOperation() {
return isImmediateOp;
}
@Override
public Response doResponseOk(final Response operationResponse) throws SubscriptionApiException, AccountApiException, CatalogApiException {
if (operationResponse.getStatus() != Status.OK.getStatusCode()) {
return operationResponse;
}
return Response.status(Status.NO_CONTENT).build();
}
};
final EntitlementCallCompletion<Response> callCompletionCreation = new EntitlementCallCompletion<Response>();
return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
}
use of org.killbill.billing.entitlement.api.Entitlement in project killbill by killbill.
the class SubscriptionResource method undoChangeSubscriptionPlan.
@TimedResource
@PUT
@Path("/{subscriptionId:" + UUID_PATTERN + "}/" + UNDO_CHANGE_PLAN)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Undo a pending change plan on an entitlement")
@ApiResponses(value = { @ApiResponse(code = 204, message = "Successful operation"), @ApiResponse(code = 400, message = "Invalid subscription id supplied"), @ApiResponse(code = 404, message = "Entitlement not found") })
public Response undoChangeSubscriptionPlan(@PathParam("subscriptionId") final UUID subscriptionId, @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) throws EntitlementApiException {
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final Entitlement current = entitlementApi.getEntitlementForId(subscriptionId, context.createCallContextNoAccountId(createdBy, reason, comment, request));
current.undoChangePlan(pluginProperties, context.createCallContextNoAccountId(createdBy, reason, comment, request));
return Response.status(Status.NO_CONTENT).build();
}
use of org.killbill.billing.entitlement.api.Entitlement in project killbill by killbill.
the class SubscriptionResource method updateSubscriptionBCD.
@TimedResource
@PUT
@Produces(APPLICATION_JSON)
@Consumes(APPLICATION_JSON)
@Path("/{subscriptionId:" + UUID_PATTERN + "}/" + BCD)
@ApiOperation(value = "Update the BCD associated to a subscription")
@ApiResponses(value = { @ApiResponse(code = 204, message = "Successful operation"), @ApiResponse(code = 400, message = "Invalid entitlement supplied") })
public Response updateSubscriptionBCD(@PathParam(ID_PARAM_NAME) final UUID subscriptionId, final SubscriptionJson json, @QueryParam(QUERY_ENTITLEMENT_EFFECTIVE_FROM_DT) final String effectiveFromDateStr, @QueryParam(QUERY_FORCE_NEW_BCD_WITH_PAST_EFFECTIVE_DATE) @DefaultValue("false") final Boolean forceNewBcdWithPastEffectiveDate, @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 UriInfo uriInfo, @javax.ws.rs.core.Context final HttpServletRequest request) throws EntitlementApiException, AccountApiException {
verifyNonNullOrEmpty(json, "SubscriptionJson body should be specified");
verifyNonNullOrEmpty(json.getBillCycleDayLocal(), "SubscriptionJson new BCD should be specified");
LocalDate effectiveFromDate = toLocalDate(effectiveFromDateStr);
final CallContext callContext = context.createCallContextNoAccountId(createdBy, reason, comment, request);
final Entitlement entitlement = entitlementApi.getEntitlementForId(subscriptionId, callContext);
if (effectiveFromDateStr != null) {
final Account account = accountUserApi.getAccountById(entitlement.getAccountId(), callContext);
final LocalDate accountToday = new LocalDate(callContext.getCreatedDate(), account.getTimeZone());
int comp = effectiveFromDate.compareTo(accountToday);
switch(comp) {
case -1:
if (!forceNewBcdWithPastEffectiveDate) {
throw new IllegalArgumentException("Changing a subscription BCD in the past may have consequences on previous invoice generated. Use flag forceNewBcdWithPastEffectiveDate to force this behavior");
}
break;
case 0:
// Ensure system will use curremt time for the event so it happens immediately
effectiveFromDate = null;
break;
case 1:
// Future date, normal case where such effectiveFromDateStr is being passed
break;
}
}
entitlement.updateBCD(json.getBillCycleDayLocal(), effectiveFromDate, callContext);
return Response.status(Status.NO_CONTENT).build();
}
use of org.killbill.billing.entitlement.api.Entitlement in project killbill by killbill.
the class SubscriptionResource method createSubscriptionsWithAddOnsInternal.
public Response createSubscriptionsWithAddOnsInternal(final List<BulkSubscriptionsBundleJson> entitlementsWithAddOns, final String entitlementDate, final String billingDate, final Boolean isMigrated, final Boolean skipResponse, final Boolean renameKeyIfExistsAndUnused, final Boolean callCompletion, final long timeoutSec, final List<String> pluginPropertiesString, final String createdBy, final String reason, final String comment, final HttpServletRequest request, final UriInfo uriInfo, final ObjectType responseObject) throws EntitlementApiException, AccountApiException, SubscriptionApiException {
Preconditions.checkArgument(Iterables.size(entitlementsWithAddOns) > 0, "No subscription specified to create");
final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
final CallContext callContextNoAccountId = context.createCallContextNoAccountId(createdBy, reason, comment, request);
Preconditions.checkArgument(Iterables.size(entitlementsWithAddOns.get(0).getBaseEntitlementAndAddOns()) > 0, "SubscriptionJson body should be specified");
final Account account = accountUserApi.getAccountById(entitlementsWithAddOns.get(0).getBaseEntitlementAndAddOns().get(0).getAccountId(), callContextNoAccountId);
final CallContext callContext = context.createCallContextWithAccountId(account.getId(), createdBy, reason, comment, request);
final Collection<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
for (final BulkSubscriptionsBundleJson subscriptionsBundleJson : entitlementsWithAddOns) {
UUID bundleId = null;
String bundleExternalKey = null;
final Collection<EntitlementSpecifier> entitlementSpecifierList = new ArrayList<EntitlementSpecifier>();
for (final SubscriptionJson entitlement : subscriptionsBundleJson.getBaseEntitlementAndAddOns()) {
// verifications
verifyNonNullOrEmpty(entitlement, "SubscriptionJson body should be specified for each element");
if (entitlement.getPlanName() == null) {
verifyNonNullOrEmpty(entitlement.getProductName(), "SubscriptionJson productName needs to be set when no planName is specified", entitlement.getProductCategory(), "SubscriptionJson productCategory needs to be set when no planName is specified", entitlement.getBillingPeriod(), "SubscriptionJson billingPeriod needs to be set when no planName is specified", entitlement.getPriceList(), "SubscriptionJson priceList needs to be set when no planName is specified");
} else {
Preconditions.checkArgument(entitlement.getProductName() == null, "SubscriptionJson productName should not be set when planName is specified");
Preconditions.checkArgument(entitlement.getProductCategory() == null, "SubscriptionJson productCategory should not be set when planName is specified");
Preconditions.checkArgument(entitlement.getBillingPeriod() == null, "SubscriptionJson billingPeriod should not be set when planName is specified");
Preconditions.checkArgument(entitlement.getPriceList() == null, "SubscriptionJson priceList should not be set when planName is specified");
}
Preconditions.checkArgument(account.getId().equals(entitlement.getAccountId()), "SubscriptionJson accountId should be the same for each element");
// If set on one element, it should be set on all elements
Preconditions.checkArgument(bundleId == null || bundleId.equals(entitlement.getBundleId()), "SubscriptionJson bundleId should be the same for each element");
if (bundleId == null) {
bundleId = entitlement.getBundleId();
}
// Can be set on a single element (e.g. BASE + ADD_ON for a new bundle)
Preconditions.checkArgument(bundleExternalKey == null || entitlement.getBundleExternalKey() == null || bundleExternalKey.equals(entitlement.getBundleExternalKey()), "SubscriptionJson externalKey should be the same for each element");
if (bundleExternalKey == null) {
bundleExternalKey = entitlement.getBundleExternalKey();
}
// create the entitlementSpecifier
final EntitlementSpecifier spec = buildEntitlementSpecifier(entitlement, account.getCurrency(), entitlement.getExternalKey());
entitlementSpecifierList.add(spec);
}
final LocalDate resolvedEntitlementDate = toLocalDate(entitlementDate);
final LocalDate resolvedBillingDate = toLocalDate(billingDate);
final BaseEntitlementWithAddOnsSpecifier baseEntitlementSpecifierWithAddOns = buildBaseEntitlementWithAddOnsSpecifier(entitlementSpecifierList, resolvedEntitlementDate, resolvedBillingDate, bundleId, bundleExternalKey, isMigrated);
baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementSpecifierWithAddOns);
}
final EntitlementCallCompletionCallback<List<UUID>> callback = new EntitlementCallCompletionCallback<List<UUID>>() {
// By default, wait for invoice and payment
// This is very 101 and won't always work though: an entitlement plugin could override dates on the fly,
// an invoice plugin could reschedule the invoice generation, etc.
private boolean isImmediateOp = true;
@Override
public List<UUID> doOperation(final CallContext ctx) throws EntitlementApiException {
for (final BaseEntitlementWithAddOnsSpecifier spec : baseEntitlementWithAddOnsSpecifierList) {
if (spec.getBillingEffectiveDate() != null) {
final boolean inTheFuture = isInTheFuture(spec.getBillingEffectiveDate(), account);
if (inTheFuture) {
// At least one subscription has a billing date in the future: don't wait for any event
// We don't support callCompletion=true for a bulk creation call with dates all over the place
isImmediateOp = false;
break;
}
}
}
return entitlementApi.createBaseEntitlementsWithAddOns(account.getId(), baseEntitlementWithAddOnsSpecifierList, renameKeyIfExistsAndUnused, pluginProperties, callContext);
}
@Override
public boolean isImmOperation() {
return isImmediateOp;
}
@Override
public Response doResponseOk(final List<UUID> entitlementIds) {
if (responseObject == ObjectType.SUBSCRIPTION) {
return uriBuilder.buildResponse(uriInfo, SubscriptionResource.class, "getSubscription", Iterables.getFirst(entitlementIds, null), request);
}
// Workaround for https://github.com/killbill/killbill/issues/1336
// While we could tweak the container to support large number of bundles in the filter (e.g. Jetty's RequestBufferSize),
// the full list is probably not that useful for the client in practice.
final Collection<String> bundleIds = new LinkedHashSet<String>();
if (!skipResponse && entitlementIds.size() < MAX_NB_SUBSCRIPTIONS_TO_FOLLOW) {
try {
for (final Entitlement entitlement : entitlementApi.getAllEntitlementsForAccountId(account.getId(), callContext)) {
if (entitlementIds.contains(entitlement.getId())) {
bundleIds.add(entitlement.getBundleId().toString());
}
}
} catch (final EntitlementApiException e) {
return Response.status(Status.INTERNAL_SERVER_ERROR).build();
}
}
if (responseObject == ObjectType.ACCOUNT) {
return uriBuilder.buildResponse(uriInfo, AccountResource.class, "getAccountBundles", account.getId(), buildBundlesFilterQueryParam(bundleIds), request);
} else if (responseObject == ObjectType.BUNDLE) {
return uriBuilder.buildResponse(uriInfo, BundleResource.class, "getBundle", Iterables.getFirst(bundleIds, null), request);
} else {
throw new IllegalStateException("Unexpected input responseObject " + responseObject);
}
}
};
final EntitlementCallCompletion<List<UUID>> callCompletionCreation = new EntitlementCallCompletion<List<UUID>>();
return callCompletionCreation.withSynchronization(callback, timeoutSec, callCompletion, callContext);
}
Aggregations