Skip to content

Commit

Permalink
subscription: Add ability to undo a pending changePlan operation. Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
sbrossie committed Nov 1, 2017
1 parent 73adef3 commit a397613
Show file tree
Hide file tree
Showing 21 changed files with 349 additions and 40 deletions.
Expand Up @@ -58,6 +58,9 @@ public boolean uncancel(final CallContext context)
public DateTime changePlan(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final CallContext context)
throws SubscriptionBaseApiException;

public boolean undoChangePlan(final CallContext context)
throws SubscriptionBaseApiException;

// Return the effective date of the change
public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, final DateTime requestedDate, final CallContext context)
throws SubscriptionBaseApiException;
Expand Down
Expand Up @@ -40,6 +40,10 @@ public enum SubscriptionBaseTransitionType {
* Occurs when a user uncancelled the {@code SubscriptionBase} before it reached its cancellation date
*/
UNCANCEL,
/**
* Occurs when a user undo a pending change before it reached its effective date
*/
UNDO_CHANGE,
/**
* Generated by the system to mark a change of phase
*/
Expand Down
Expand Up @@ -52,6 +52,7 @@
import org.killbill.billing.entitlement.engine.core.EntitlementNotificationKeyAction;
import org.killbill.billing.entitlement.engine.core.EntitlementUtils;
import org.killbill.billing.entitlement.engine.core.EventsStreamBuilder;
import org.killbill.billing.entitlement.logging.EntitlementLoggingHelper;
import org.killbill.billing.entitlement.plugin.api.EntitlementContext;
import org.killbill.billing.entitlement.plugin.api.OperationType;
import org.killbill.billing.entity.EntityBase;
Expand Down Expand Up @@ -80,6 +81,7 @@
import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logCancelEntitlement;
import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logChangePlan;
import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUncancelEntitlement;
import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUndoChangePlan;
import static org.killbill.billing.entitlement.logging.EntitlementLoggingHelper.logUpdateBCD;

public class DefaultEntitlement extends EntityBase implements Entitlement {
Expand Down Expand Up @@ -393,7 +395,7 @@ public void uncancelEntitlement(final Iterable<PluginProperty> properties, final
false);
final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementWithAddOnsSpecifier);
final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNCANCEL_SUBSCRIPTION,
final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNDO_PENDING_SUBSCRIPTION_OPERATION,
getAccountId(),
null,
baseEntitlementWithAddOnsSpecifierList,
Expand Down Expand Up @@ -611,6 +613,51 @@ public Entitlement doCall(final EntitlementApi entitlementApi, final Entitlement
return pluginExecution.executeWithPlugin(changePlanWithPlugin, pluginContext);
}

@Override
public void undoChangePlan(final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {

logUndoChangePlan(log, this);

checkForPermissions(Permission.ENTITLEMENT_CAN_CHANGE_PLAN, callContext);

// Get the latest state from disk
refresh(callContext);

final BaseEntitlementWithAddOnsSpecifier baseEntitlementWithAddOnsSpecifier = new DefaultBaseEntitlementWithAddOnsSpecifier(
getBundleId(),
getExternalKey(),
null,
null,
null,
false);
final List<BaseEntitlementWithAddOnsSpecifier> baseEntitlementWithAddOnsSpecifierList = new ArrayList<BaseEntitlementWithAddOnsSpecifier>();
baseEntitlementWithAddOnsSpecifierList.add(baseEntitlementWithAddOnsSpecifier);
final EntitlementContext pluginContext = new DefaultEntitlementContext(OperationType.UNDO_PENDING_SUBSCRIPTION_OPERATION,
getAccountId(),
null,
baseEntitlementWithAddOnsSpecifierList,
null,
properties,
callContext);

final WithEntitlementPlugin<Void> undoChangePlanEntitlementWithPlugin = new WithEntitlementPlugin<Void>() {

@Override
public Void doCall(final EntitlementApi entitlementApi, final EntitlementContext updatedPluginContext) throws EntitlementApiException {

try {
getSubscriptionBase().undoChangePlan(callContext);
} catch (final SubscriptionBaseApiException e) {
throw new EntitlementApiException(e);
}
return null;
}
};

pluginExecution.executeWithPlugin(undoChangePlanEntitlementWithPlugin, pluginContext);

}

@Override
public Entitlement changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides, @Nullable final LocalDate effectiveDate, final Iterable<PluginProperty> properties, final CallContext callContext) throws EntitlementApiException {

Expand Down
Expand Up @@ -214,6 +214,16 @@ public static void logUncancelEntitlement(final Logger log, final Entitlement en
}
}

public static void logUndoChangePlan(final Logger log, final Entitlement entitlement) {
if (log.isInfoEnabled()) {
final StringBuilder logLine = new StringBuilder("Undo Entitlement Change Plan: ")
.append(" id = '")
.append(entitlement.getId())
.append("'");
log.info(logLine.toString());
}
}

public static void logChangePlan(final Logger log, final Entitlement entitlement, final PlanSpecifier spec,
final List<PlanPhasePriceOverride> overrides, final LocalDate entitlementEffectiveDate, final BillingActionPolicy actionPolicy) {
if (log.isInfoEnabled()) {
Expand Down
Expand Up @@ -260,6 +260,10 @@ public interface JaxrsResource {

public static final String CBA_REBALANCING = "cbaRebalancing";


public static final String UNDO_CHANGE_PLAN = "undoChangePlan";
public static final String UNDO_CANCEL = "uncancel";

public static final String PAUSE = "pause";
public static final String RESUME = "resume";
public static final String BLOCK = "block";
Expand Down
Expand Up @@ -499,7 +499,7 @@ private Map<String, String> buildQueryParams(final List<String> bundleIdList) {

@TimedResource
@PUT
@Path("/{subscriptionId:" + UUID_PATTERN + "}/uncancel")
@Path("/{subscriptionId:" + UUID_PATTERN + "}/" + UNDO_CANCEL)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Un-cancel an entitlement")
@ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid subscription id supplied"),
Expand All @@ -517,6 +517,26 @@ public Response uncancelEntitlementPlan(@PathParam("subscriptionId") final Strin
return Response.status(Status.OK).build();
}

@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 = 400, message = "Invalid subscription id supplied"),
@ApiResponse(code = 404, message = "Entitlement not found")})
public Response undoChangeEntitlementPlan(@PathParam("subscriptionId") final String 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 UUID uuid = UUID.fromString(subscriptionId);
final Entitlement current = entitlementApi.getEntitlementForId(uuid, context.createCallContextNoAccountId(createdBy, reason, comment, request));
current.undoChangePlan(pluginProperties, context.createCallContextNoAccountId(createdBy, reason, comment, request));
return Response.status(Status.OK).build();
}

@TimedResource
@PUT
@Produces(APPLICATION_JSON)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
<version>0.141.5</version>
<version>0.141.6-SNAPSHOT</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.19.0-SNAPSHOT</version>
Expand Down
Expand Up @@ -669,8 +669,61 @@ public void testEntitlementUsingPlanName() throws Exception {
Assert.assertEquals(newEntitlementJson.getBillingPeriod(), BillingPeriod.MONTHLY);
Assert.assertEquals(newEntitlementJson.getPriceList(), DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
Assert.assertEquals(newEntitlementJson.getPlanName(), "pistol-monthly");
}

@Test(groups = "slow", description = "Can changePlan and undo changePlan on a subscription")
public void testEntitlementUndoChangePlan() throws Exception {
final DateTime initialDate = new DateTime(2012, 4, 25, 0, 3, 42, 0);
clock.setDeltaFromReality(initialDate.getMillis() - clock.getUTCNow().getMillis());

final Account accountJson = createAccountWithDefaultPaymentMethod();

final String productName = "Shotgun";
final BillingPeriod term = BillingPeriod.MONTHLY;

final Subscription entitlementJson = createEntitlement(accountJson.getAccountId(), "99999", productName,
ProductCategory.BASE, term, true);



// Change plan in the future
final String newProductName = "Assault-Rifle";

final Subscription newInput = new Subscription();
newInput.setAccountId(entitlementJson.getAccountId());
newInput.setSubscriptionId(entitlementJson.getSubscriptionId());
newInput.setProductName(newProductName);
newInput.setProductCategory(ProductCategory.BASE);
newInput.setBillingPeriod(entitlementJson.getBillingPeriod());
newInput.setPriceList(entitlementJson.getPriceList());

Subscription refreshedSubscription = killBillClient.updateSubscription(newInput, new LocalDate(2012, 4, 28), null, CALL_COMPLETION_TIMEOUT_SEC, requestOptions);
Assert.assertNotNull(refreshedSubscription);


final Interval it = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(1));
clock.addDeltaFromReality(it.toDurationMillis());

killBillClient.undoChangePlan(refreshedSubscription.getSubscriptionId(), requestOptions);

// MOVE AFTER TRIAL
final Interval it2 = new Interval(clock.getUTCNow(), clock.getUTCNow().plusDays(30));
clock.addDeltaFromReality(it2.toDurationMillis());

crappyWaitForLackOfProperSynchonization();

// Retrieves to check EndDate
refreshedSubscription = killBillClient.getSubscription(entitlementJson.getSubscriptionId(), requestOptions);
Assert.assertEquals(refreshedSubscription.getPriceOverrides().size(), 2);
Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(0).getPhaseName(), "shotgun-monthly-trial");
Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(0).getFixedPrice(), BigDecimal.ZERO);
Assert.assertNull(refreshedSubscription.getPriceOverrides().get(0).getRecurringPrice());
Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(1).getPhaseName(), "shotgun-monthly-evergreen");
Assert.assertNull(refreshedSubscription.getPriceOverrides().get(1).getFixedPrice());
Assert.assertEquals(refreshedSubscription.getPriceOverrides().get(1).getRecurringPrice(), new BigDecimal("249.95"));

}



}
Expand Up @@ -117,4 +117,6 @@ public List<SubscriptionBaseEvent> getEventsOnCancelPlan(final DefaultSubscripti
final boolean addCancellationAddOnForEventsIfRequired,
final Catalog fullCatalog,
final InternalTenantContext internalTenantContext) throws CatalogApiException;

boolean undoChangePlan(DefaultSubscriptionBase defaultSubscriptionBase, CallContext context) throws SubscriptionBaseApiException;
}
Expand Up @@ -41,7 +41,6 @@
import org.killbill.billing.catalog.api.PlanPhase;
import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
import org.killbill.billing.catalog.api.PlanSpecifier;
import org.killbill.billing.catalog.api.PriceList;
import org.killbill.billing.catalog.api.Product;
import org.killbill.billing.catalog.api.ProductCategory;
Expand Down Expand Up @@ -279,6 +278,11 @@ public DateTime changePlan(final PlanPhaseSpecifier spec,
return apiService.changePlan(this, spec, overrides, context);
}

@Override
public boolean undoChangePlan(final CallContext context) throws SubscriptionBaseApiException {
return apiService.undoChangePlan(this, context);
}

@Override
public DateTime changePlanWithDate(final PlanPhaseSpecifier spec, final List<PlanPhasePriceOverride> overrides,
final DateTime requestedDate, final CallContext context) throws SubscriptionBaseApiException {
Expand Down Expand Up @@ -501,10 +505,10 @@ public SubscriptionBaseTransitionData getTransitionFromEvent(final SubscriptionB
prev = curData;
}
}
// Since UNCANCEL are not part of the transitions, we compute a new 'UNCANCEL' transition based on the event right before that UNCANCEL
// This is used to be able to send a bus event for uncancellation
if (prev != null && event.getType() == EventType.API_USER && ((ApiEvent) event).getApiEventType() == ApiEventType.UNCANCEL) {
final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData((SubscriptionBaseTransitionData) prev, EventType.API_USER, ApiEventType.UNCANCEL, seqId);
// Since UNCANCEL/UNDO_CHANGE are not part of the transitions, we compute a new transition based on the event right before
// This is used to be able to send a bus event for uncancellation/undo_change
if (prev != null && event.getType() == EventType.API_USER && (((ApiEvent) event).getApiEventType() == ApiEventType.UNCANCEL || ((ApiEvent) event).getApiEventType() == ApiEventType.UNDO_CHANGE)) {
final SubscriptionBaseTransitionData withSeq = new SubscriptionBaseTransitionData(prev, EventType.API_USER, ((ApiEvent) event).getApiEventType(), seqId);
return withSeq;
}
return null;
Expand Down Expand Up @@ -569,10 +573,18 @@ public SubscriptionBaseTransitionData getLastTransitionForCurrentPlan() {
throw new SubscriptionBaseError(String.format("Failed to find InitialTransitionForCurrentPlan id = %s", getId()));
}

public boolean isSubscriptionFutureCancelled() {
public boolean isFutureCancelled() {
return getFutureEndDate() != null;
}


public boolean isPendingChangePlan() {
final SubscriptionBaseTransition pendingTransition = getPendingTransition();
return pendingTransition != null && pendingTransition.getTransitionType() == SubscriptionBaseTransitionType.CHANGE;
}



public DateTime getPlanChangeEffectiveDate(final BillingActionPolicy policy, @Nullable final BillingAlignment alignment, @Nullable final Integer accountBillCycleDayLocal, final InternalTenantContext context) {

final DateTime candidateResult;
Expand Down Expand Up @@ -733,6 +745,7 @@ public void rebuildTransitions(final List<SubscriptionBaseEvent> inputEvents, fi
nextPhaseName = null;
break;
case UNCANCEL:
case UNDO_CHANGE:
default:
throw new SubscriptionBaseError(String.format(
"Unexpected UserEvent type = %s", userEV
Expand Down

1 comment on commit a397613

@pierre
Copy link
Member

@pierre pierre commented on a397613 Nov 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Please sign in to comment.