bug5260: add schedule background task for fetching and updating user subscriptions

This commit is contained in:
Tu Tran
2020-11-21 17:36:32 +07:00
parent 3f5c3ef8e2
commit 1b4ad3471e
13 changed files with 218 additions and 29 deletions
@@ -9,8 +9,6 @@ import java.util.logging.Logger;
import javax.servlet.ServletConfig;
import org.apache.commons.lang.StringUtils;
import com.chargebee.Result;
import com.chargebee.models.HostedPage;
import com.chargebee.models.HostedPage.Content;
@@ -30,7 +28,6 @@ import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.subscription.Subscription;
import com.sap.sse.security.shared.subscription.SubscriptionPlan;
import com.sap.sse.security.shared.subscription.chargebee.ChargebeeSubscription;
import com.sap.sse.security.shared.subscription.chargebee.ChargebeeSubscriptionProvider;
import com.sap.sse.security.subscription.chargebee.ChargebeeApiService;
/**
@@ -59,8 +56,8 @@ public class ChargebeeSubscriptionServiceImpl extends BaseSubscriptionServiceImp
.billingAddressLastName(usernames.getB()).billingAddressCountry("US").request();
response.setHostedPageJSONString(result.hostedPage().toJson());
} else {
response.setError("User has already subscribed to "
+ SubscriptionPlan.getPlan(planId).getName() + " plan");
response.setError(
"User has already subscribed to " + SubscriptionPlan.getPlan(planId).getName() + " plan");
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Error in generating Chargebee hosted page data ", e);
@@ -118,7 +115,8 @@ public class ChargebeeSubscriptionServiceImpl extends BaseSubscriptionServiceImp
if (subscriptions != null) {
List<SubscriptionItem> itemList = new ArrayList<SubscriptionItem>();
for (Subscription subscription : subscriptions) {
if (StringUtils.isNotEmpty(subscription.getSubscriptionId())) {
if (subscription.hasSubscriptionId() && !subscription.getSubscriptionStatus()
.equals(ChargebeeSubscription.SUBSCRIPTION_STATUS_CANCELLED)) {
itemList.add(
new ChargebeeSubscriptionItem(subscription.getPlanId(), subscription.getTrialStart(),
subscription.getTrialEnd(), subscription.getSubscriptionStatus(),
@@ -148,8 +146,14 @@ public class ChargebeeSubscriptionServiceImpl extends BaseSubscriptionServiceImp
Result resultData = cancel(subscription.getSubscriptionId()).request();
if (resultData.subscription().status().name().toLowerCase()
.equals(ChargebeeSubscription.SUBSCRIPTION_STATUS_CANCELLED)) {
Subscription newSubscription = ChargebeeSubscription.createEmptySubscription(
subscription.getPlanId(), subscription.getLatestEventTime(), TimePoint.now());
Subscription newSubscription = new ChargebeeSubscription(subscription.getSubscriptionId(),
subscription.getPlanId(), subscription.getCustomerId(), subscription.getTrialStart(),
subscription.getTrialEnd(), ChargebeeSubscription.SUBSCRIPTION_STATUS_CANCELLED,
subscription.getPaymentStatus(), subscription.getTransactionType(),
subscription.getTransactionStatus(), subscription.getInvoiceId(),
subscription.getInvoiceStatus(), subscription.getSubscriptionCreatedAt(),
TimePoint.of(resultData.subscription().updatedAt()), subscription.getLatestEventTime(),
TimePoint.now());
updateUserSubscription(user, newSubscription);
result = true;
} else {
@@ -103,7 +103,6 @@ public class ChargebeeWebHookHandler extends SubscriptionWebHookHandler {
case CUSTOMER_DELETED:
updateUserSubscription(user, buildEmptySubscription(userSubscription, event));
break;
case SUBSCRIPTION_CANCELLED:
case SUBSCRIPTION_DELETED:
if (userSubscription != null && userSubscription.getSubscriptionId() != null
&& userSubscription.getSubscriptionId().equals(event.getSubscriptionId())) {
@@ -117,6 +116,7 @@ public class ChargebeeWebHookHandler extends SubscriptionWebHookHandler {
case PAYMENT_FAILED:
case SUBSCRIPTION_PAUSED:
case SUBSCRIPTION_RESUMED:
case SUBSCRIPTION_CANCELLED:
updateUserSubscription(user, buildSubscription(userSubscription, event));
break;
case PAYMENT_REFUNDED:
@@ -201,12 +201,24 @@ public abstract class Subscription implements Serializable {
return planId != null && !planId.isEmpty();
}
public boolean hasSubscriptionId() {
return subscriptionId != null && !subscriptionId.isEmpty();
}
/**
* Check if subscription is active, base on this user will gain roles for the subscription
*
* @return true if status subscription is in trial or status is active and user has success payment
*/
public abstract boolean isActiveSubscription();
/**
* Check if subscription is updated more recently than other subscription
*/
public boolean isUpdatedMoreRecently(Subscription otherSubscription) {
return getManualUpdatedAt().asMillis() > otherSubscription.getManualUpdatedAt().asMillis()
|| getLatestEventTime().asMillis() > otherSubscription.getLatestEventTime().asMillis();
}
@Override
public String toString() {
@@ -1,5 +1,6 @@
package com.sap.sse.security.shared.subscription;
import java.util.HashMap;
import java.util.Map;
import com.sap.sse.common.TimePoint;
@@ -25,7 +26,21 @@ public class SubscriptionData {
MANUAL_UPDATED_AT,
PROVIDER;
}
/**
* Create empty subscription data instance that only holds plan id, latest webhook event update time, and manuall
* updated time. This subscription is used for keeping updated time of subscription for a user, then it would
* prevent issue of outdated data be persisted
*/
public static SubscriptionData createEmptySubscriptionDataWithUpdateTimes(String planId, TimePoint latestEventTime,
TimePoint manualUpdatedTime) {
Map<String, Object> data = new HashMap<String, Object>();
data.put(DataAttribute.PLAN_ID.name(), planId);
data.put(DataAttribute.LATEST_EVENT_TIME.name(), latestEventTime.asMillis());
data.put(DataAttribute.MANUAL_UPDATED_AT.name(), manualUpdatedTime.asMillis());
return new SubscriptionData(data);
}
private Map<String, Object> data;
public SubscriptionData(Map<String, Object> data) {
@@ -15,5 +15,5 @@ public interface SubscriptionDataHandler {
/**
* Restore subscription from data
*/
Subscription restore(SubscriptionData data);
Subscription toSubscription(SubscriptionData data);
}
@@ -7,7 +7,7 @@ import com.sap.sse.security.shared.subscription.SubscriptionData;
public class ChargebeeSubscriptionDataHandler extends AbstractSubscriptionDataHandler {
@Override
public Subscription restore(SubscriptionData data) {
public Subscription toSubscription(SubscriptionData data) {
return new ChargebeeSubscription(data.getSubscriptionId(), data.getPlanId(), data.getCustomerId(),
data.getTrialStart(), data.getTrialEnd(), data.getSubscriptionStatus(), data.getPaymentStatus(),
data.getTransactionType(), data.getTransactionStatus(), data.getInvoiceId(), data.getInvoiceStatus(),
@@ -526,7 +526,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
try {
final SubscriptionProvider subscriptionProvider = SubscriptionFactory.getInstance()
.getSubscriptionProvider(subscriptionData.getProvider());
subscriptions[i++] = subscriptionProvider.getDataHandler().restore(subscriptionData);
subscriptions[i++] = subscriptionProvider.getDataHandler().toSubscription(subscriptionData);
} catch (InvalidSubscriptionProviderException e) {
logger.log(Level.SEVERE, "Failed to load subscription for user " + subscriptionData.getCustomerId(),
e);
@@ -661,5 +661,10 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
* Persist user subscription data
*/
void updateUserSubscription(String username, Subscription subscription) throws UserManagementException;
/**
* Remove all user subscriptions of a payment service provider
*/
void removeProviderUserSubscriptions(String username, String providerName) throws UserManagementException;
}
@@ -112,5 +112,7 @@ public interface ReplicableSecurityService extends SecurityService {
Void internalResetPassword(String username, String passwordResetSecret);
Void internalUpdateSubscription(String username, Subscription newSubscription) throws UserManagementException;
Void internalRemoveProviderSubscriptions(String username, String provider) throws UserManagementException;
}
@@ -123,6 +123,7 @@ import com.sap.sse.security.operations.DeleteUserOperation;
import com.sap.sse.security.operations.PutRoleDefinitionToUserGroupOperation;
import com.sap.sse.security.operations.RemoveAccessTokenOperation;
import com.sap.sse.security.operations.RemovePermissionForUserOperation;
import com.sap.sse.security.operations.RemoveProviderSubscriptionsOperation;
import com.sap.sse.security.operations.RemoveRoleDefinitionFromUserGroupOperation;
import com.sap.sse.security.operations.RemoveRoleFromUserOperation;
import com.sap.sse.security.operations.RemoveUserFromUserGroupOperation;
@@ -2509,18 +2510,52 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
if (user != null) {
String newSubscriptionPlanId = newSubscription.getPlanId();
Subscription currentSubscription = user.getSubscriptionByPlan(newSubscriptionPlanId);
logger.info(() -> "Update user subscription for plan " + newSubscriptionPlanId);
logger.info(() -> "Current user plan subscription: "
+ (currentSubscription != null ? currentSubscription.toString() : "null"));
logger.info(
() -> "New plan subscription: " + (newSubscription != null ? newSubscription.toString() : "null"));
if (shouldUpdateUserRolesForSubscription(user, currentSubscription, newSubscription)) {
updateUserRolesOnSubscriptionChange(user, currentSubscription, newSubscription);
if (shouldProcessNewSubscription(currentSubscription, newSubscription)) {
logger.info(() -> "Update user subscription for plan " + newSubscriptionPlanId);
logger.info(() -> "Current user plan subscription: "
+ (currentSubscription != null ? currentSubscription.toString() : "null"));
logger.info(() -> "New plan subscription: "
+ (newSubscription != null ? newSubscription.toString() : "null"));
if (shouldUpdateUserRolesForSubscription(user, currentSubscription, newSubscription)) {
updateUserRolesOnSubscriptionChange(user, currentSubscription, newSubscription);
}
Subscription[] newSubscriptions = buildNewUserSubscriptions(user, newSubscription);
if (newSubscriptions != null) {
user.setSubscriptions(newSubscriptions);
}
store.updateUser(user);
} else {
logger.info(() -> "New subscription has been ignored: " + newSubscription);
}
Subscription[] newSubscriptions = buildNewUserSubscriptions(user, newSubscription);
if (newSubscriptions != null) {
user.setSubscriptions(newSubscriptions);
return null;
} else {
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
}
@Override
public void removeProviderUserSubscriptions(String username, String providerName) throws UserManagementException {
final User user = getUserByName(username);
if (user != null) {
apply(new RemoveProviderSubscriptionsOperation(username, providerName));
} else {
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
}
@Override
public Void internalRemoveProviderSubscriptions(String username, String providerName)
throws UserManagementException {
User user = getUserByName(username);
if (user != null) {
Iterable<Subscription> subscriptions = user.getSubscriptions();
List<Subscription> newSubscriptionList = new ArrayList<Subscription>();
for (Subscription subscription : subscriptions) {
if (!subscription.getProvider().equals(providerName)) {
newSubscriptionList.add(subscription);
}
}
user.setSubscriptions(newSubscriptionList.toArray(new Subscription[] {}));
store.updateUser(user);
return null;
} else {
@@ -2528,6 +2563,36 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
}
}
/**
* Check if new subscription should be processed, such as if it has a valid plan, or it's the most recent
* subscription of a plan
*/
private boolean shouldProcessNewSubscription(Subscription currentSubscription, Subscription newSubscription) {
final boolean shouldProcess;
if (!newSubscription.hasPlan()) {
// New subscription doesn't have plan id, that means it's an empty subscription model which is used for
// clearing all user subscriptions
shouldProcess = true;
} else if (SubscriptionPlan.getPlan(newSubscription.getPlanId()) != null) {
if (currentSubscription == null) {
// New subscription plan is valid, but current subscription of the plan is empty
shouldProcess = true;
} else if (!newSubscription.hasSubscriptionId()) {
// New subscription has plan id but doesn't have subscription id, this is an empty subscription for the
// plan that holds updated dates data for the plan's subscription
shouldProcess = newSubscription.isUpdatedMoreRecently(currentSubscription);
} else {
// Only process new subscription if it is the most recently created subscription
shouldProcess = newSubscription.getSubscriptionCreatedAt().asMillis() >= currentSubscription
.getSubscriptionCreatedAt().asMillis();
}
} else {
// New subscription doesn't have a valid plan
shouldProcess = false;
}
return shouldProcess;
}
/**
* Build new subscription list for user from new subscription. This might update a subscription model, or add new
* one to user's subscription list. In case no updates for current user subscriptions then null will be returned
@@ -2536,13 +2601,27 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
Subscription[] newUserSubscriptions = null;
Iterable<Subscription> subscriptions = user.getSubscriptions();
if (newSubscription != null) {
if (subscriptions == null || !subscriptions.iterator().hasNext() || !newSubscription.hasPlan()) {
if (subscriptions == null || !subscriptions.iterator().hasNext()) {
newUserSubscriptions = new Subscription[] { newSubscription };
} else if (!newSubscription.hasPlan()) {
// New subscription has no plan, that means new subscription is just an empty one with some meta data
// for updated dates, and user has been deleted from provider. In this case we need to remove all
// current subscriptions of the user for this provider
List<Subscription> newSubscriptionList = new ArrayList<Subscription>();
for (Subscription subscription : subscriptions) {
if (!subscription.getProvider().equals(newSubscription.getProvider())) {
newSubscriptionList.add(subscription);
}
}
newSubscriptionList.add(newSubscription);
newUserSubscriptions = newSubscriptionList.toArray(new Subscription[] {});
} else {
List<Subscription> newSubscriptionList = new ArrayList<Subscription>();
boolean foundCurrentSubscription = false;
for (Subscription subscription : subscriptions) {
if (!subscription.hasPlan() || subscription.getPlanId().equals(newSubscription.getPlanId())) {
if (!foundCurrentSubscription && ((!subscription.hasPlan()
&& subscription.getProvider().equals(newSubscription.getProvider()))
|| subscription.getPlanId().equals(newSubscription.getPlanId()))) {
newSubscriptionList.add(newSubscription);
foundCurrentSubscription = true;
} else {
@@ -0,0 +1,24 @@
package com.sap.sse.security.operations;
import com.sap.sse.security.impl.ReplicableSecurityService;
/**
* Remove user subscriptions of a provider operation
*/
public class RemoveProviderSubscriptionsOperation implements SecurityOperation<Void> {
private static final long serialVersionUID = -3121534374405832205L;
private String username;
private String providerName;
public RemoveProviderSubscriptionsOperation(String username, String providerName) {
this.username = username;
this.providerName = providerName;
}
@Override
public Void internalApplyTo(ReplicableSecurityService toState) throws Exception {
toState.internalRemoveProviderSubscriptions(username, providerName);
return null;
}
}
@@ -1,13 +1,18 @@
package com.sap.sse.security.subscription;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sap.sse.common.TimePoint;
import com.sap.sse.security.SecurityService;
import com.sap.sse.security.shared.UserManagementException;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.shared.subscription.Subscription;
import com.sap.sse.security.shared.subscription.SubscriptionData;
import com.sap.sse.security.shared.subscription.SubscriptionFactory;
import com.sap.sse.security.shared.subscription.SubscriptionProvider;
@@ -34,13 +39,14 @@ public class SubscriptionUpdateTask implements Runnable {
}
private void fetchAndUserSubscriptions(User user, Iterable<SubscriptionProvider> subscriptionProviders) {
logger.info(() -> "Start checking and updating subscriptions for user: " + user.getName());
for (SubscriptionProvider provider : subscriptionProviders) {
SubscriptionApiService apiService = SubscriptionServiceFactory.getInstance()
.getApiService(provider.getProviderName());
if (apiService != null) {
try {
Iterable<Subscription> userSubscriptions = apiService.getUserSubscriptions(user);
checkAndUpdateSubscriptions(user, userSubscriptions);
checkAndUpdateSubscriptions(user, userSubscriptions, provider);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to fetch and update subscriptions for user " + user.getName(), e);
}
@@ -48,8 +54,50 @@ public class SubscriptionUpdateTask implements Runnable {
}
}
private void checkAndUpdateSubscriptions(User user, Iterable<Subscription> userSubscriptions) {
// TODO: implement checking and updating subscriptions
private void checkAndUpdateSubscriptions(User user, Iterable<Subscription> userSubscriptions,
SubscriptionProvider provider) throws UserManagementException {
logger.info(() -> "Subscriptions from provider " + provider.getProviderName() + " for user " + user.getName()
+ ": " + userSubscriptions);
Iterable<Subscription> currentSubscriptions = user.getSubscriptions();
if ((userSubscriptions == null || !userSubscriptions.iterator().hasNext())
&& (currentSubscriptions != null && currentSubscriptions.iterator().hasNext())) {
// No subscriptions so we need to remove all current subscriptions of user for the provider
Subscription emptySubscription = createEmptySubscription(provider, null);
getSecurityService().updateUserSubscription(user.getName(), emptySubscription);
// getSecurityService().removeProviderUserSubscriptions(user.getName(), providerName);
} else if (userSubscriptions != null) {
if (currentSubscriptions != null) {
Map<String, Boolean> existingPlans = getExistingPlans(userSubscriptions);
for (Subscription subscription : currentSubscriptions) {
if (subscription.hasPlan() && !existingPlans.containsKey(subscription.getPlanId())) {
// Current subscription plan doesn't exist in subscription list from provider, that means
// subscription for the plan has been deleted, then we need to remove the subscription from
// database
Subscription emptySubscription = createEmptySubscription(provider, subscription.getPlanId());
getSecurityService().updateUserSubscription(user.getName(), emptySubscription);
}
}
}
for (Subscription subscription : userSubscriptions) {
getSecurityService().updateUserSubscription(user.getName(), subscription);
}
}
}
private Subscription createEmptySubscription(SubscriptionProvider provider, String planId) {
return provider.getDataHandler().toSubscription(
SubscriptionData.createEmptySubscriptionDataWithUpdateTimes(planId, TimePoint.now(), TimePoint.now()));
}
private Map<String, Boolean> getExistingPlans(Iterable<Subscription> subscriptions) {
final Map<String, Boolean> existingPlans = new HashMap<String, Boolean>();
for (Subscription subscription : subscriptions) {
if (subscription.hasPlan()) {
existingPlans.put(subscription.getPlanId(), true);
}
}
return existingPlans;
}
private SecurityService getSecurityService() {
@@ -88,7 +88,7 @@ public class ChargebeeApiService implements SubscriptionApiService {
*/
private Pair<Iterable<SubscriptionItem>, String> fetchSubscriptions(User user, String offset) throws Exception {
SubscriptionListRequest request = com.chargebee.models.Subscription.list().limit(100).customerId()
.is(user.getName()).includeDeleted(false);
.is(user.getName()).includeDeleted(false).sortByCreatedAt(SortOrder.DESC);
if (offset != null && !offset.isEmpty()) {
request.offset(offset);
}