Payment: Refactor code, add Shiro basic authentication filter for webhooks

This commit is contained in:
Tu Tran
2020-05-21 17:02:56 +07:00
parent 655756e3b9
commit 9b439686a1
14 changed files with 368 additions and 226 deletions
@@ -35,7 +35,6 @@ public class UserProfileSubscriptionViewImpl extends Composite implements UserPr
@Override
public NeedsAuthenticationContext getDecorator() {
// TODO Auto-generated method stub
return decoratorUi;
}
@@ -14,15 +14,17 @@ import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Widget;
import com.sap.sailing.gwt.home.shared.places.user.profile.subscription.UserSubscriptionView;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionPlans;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionPlans.Plan;
import com.sap.sse.gwt.client.Notification;
import com.sap.sse.gwt.client.Notification.NotificationType;
/**
* View for displaying user subscription information like plan, subscription status...In this view user is able to
@@ -92,7 +94,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
@Override
public void onOpenCheckoutError(String error) {
updateSubscriptionButtonUi.setEnabled(true);
Window.alert(error);
Notification.notify(error, NotificationType.ERROR);
}
@Override
@@ -110,7 +112,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
Plan plan = null;
if (subscription != null) {
plan = SubscriptionPlans.getPlan(subscription.planId);
plan = SubscriptionPlans.getPlan(subscription.getPlanId());
}
if (subscription == null || plan == null) {
@@ -149,28 +151,26 @@ public class UserSubscription extends Composite implements UserSubscriptionView
planListUi.setSelectedIndex(0);
}
List<Plan> planList = SubscriptionPlans.getPlanList();
for (int i = 0; i < planList.size(); i++) {
Plan plan = planList.get(i);
int i = 0;
for (Plan plan : planList) {
planListUi.addItem(plan.getName(), plan.getId());
if (subscription != null && subscription.planId.equals(plan.getId())) {
if (subscription != null && subscription.getPlanId().equals(plan.getId())) {
planListUi.setSelectedIndex(i);
}
i++;
}
}
private String buildTrialText(SubscriptionDTO subscription) {
long now = Math.round(Duration.currentTimeMillis() / 1000);
long remain = subscription.trialEnd - now;
long remain = subscription.getTrialEnd() - now;
StringBuilder remainText = new StringBuilder();
if (remain <= 0) {
remainText.append("0 day");
remainText.append(StringMessages.INSTANCE.numHours(0));
} else {
long days = remain / 86400;
int days = (int) (remain / 86400);
if (days > 0) {
remainText.append(days).append(" day");
if (days > 1) {
remainText.append("s");
}
remainText.append(StringMessages.INSTANCE.numDays(days));
}
remain = remain % 86400;
int hours = (int) (remain / 3600);
@@ -178,10 +178,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
if (remainText.length() > 0) {
remainText.append(" ");
}
remainText.append(hours).append(" hour");
if (hours > 1) {
remainText.append("s");
}
remainText.append(StringMessages.INSTANCE.numHours(hours));
}
if (days == 0) {
remain = remain % 3600;
@@ -190,24 +187,17 @@ public class UserSubscription extends Composite implements UserSubscriptionView
if (remainText.length() > 0) {
remainText.append(" ");
}
remainText.append(mins).append(" min");
if (mins > 1) {
remainText.append("s");
}
remainText.append(StringMessages.INSTANCE.numMinutes(mins));
}
}
}
if (remainText.length() == 0) {
remainText.append("0 day");
remainText.append(StringMessages.INSTANCE.numHours(0));
}
StringBuilder trialText = new StringBuilder();
trialText.append("Your trial expires in ").append(remainText.toString()).append(" (").append("ends on ")
.append(DateTimeFormat.getFormat("yyyy-MM-dd HH:mm").format(new Date(subscription.trialEnd * 1000)))
.append(")");
return trialText.toString();
return StringMessages.INSTANCE.trialText(remainText.toString(),
DateTimeFormat.getFormat("yyyy-MM-dd HH:mm").format(new Date(subscription.getTrialEnd() * 1000)));
}
private void resetElementsVisibleState() {
@@ -1,8 +1,8 @@
package com.sap.sailing.gwt.home.shared.places.user.profile.subscription;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sailing.gwt.home.shared.app.ClientFactoryWithDispatch;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory;
import com.sap.sailing.gwt.ui.client.subscription.Chargebee;
import com.sap.sailing.gwt.ui.client.subscription.CheckoutOption;
@@ -10,35 +10,30 @@ import com.sap.sailing.gwt.ui.client.subscription.SubscriptionConfiguration;
import com.sap.sailing.gwt.ui.client.subscription.WithSubscriptionService;
import com.sap.sailing.gwt.ui.shared.subscription.HostedPageResultDTO;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
import com.sap.sse.gwt.client.Notification;
import com.sap.sse.gwt.client.Notification.NotificationType;
import com.sap.sse.security.ui.authentication.WithAuthenticationManager;
import com.sap.sse.security.ui.authentication.WithUserService;
/**
* Presenter for {@link UserSubscriptionView}, implementation of {@link UserSubscriptionView.Presenter}}, which handles
* initializing Chargebee, opening and executing checkout, requesting user subscription data, and canceling user
* subscription
*
* @author tutran
*/
public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & ErrorAndBusyClientFactory & WithAuthenticationManager & WithUserService & WithSubscriptionService>
implements UserSubscriptionView.Presenter {
private final C clientFactory;
private UserSubscriptionView view;
/**
* Callback for Chargebee checkout success event
* Checkout success callback
*/
private CheckoutOption.SuccessCallback onCheckoutSuccessCallback = new CheckoutOption.SuccessCallback() {
@Override
public void call(String hostedPageId) {
requestFinishingPlanUpgrading(hostedPageId);
requestFinishingPlanUpdating(hostedPageId);
}
};
/**
* Callback for Chargebee checkout fail event
* Checkout fail callback
*/
private CheckoutOption.ErrorCallback onCheckoutErrorCallback = new CheckoutOption.ErrorCallback() {
@@ -49,7 +44,7 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
};
/**
* Callback for Chargebee checkout modal close event
* Checkout modal close callback
*/
private CheckoutOption.CloseCallback onCheckoutCloseCallback = new CheckoutOption.CloseCallback() {
@@ -76,8 +71,8 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void onSuccess(SubscriptionDTO result) {
if (result != null && result.error != null && !result.error.isEmpty()) {
Window.alert("Get user subscription error: " + result.error);
if (result != null && result.getError() != null && !result.getError().isEmpty()) {
showError(StringMessages.INSTANCE.errorLoadingUserSubscription(result.getError()));
return;
}
@@ -86,7 +81,7 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void onFailure(Throwable caught) {
Window.alert("Get user subscription failed: " + caught.getMessage());
showError(StringMessages.INSTANCE.errorLoadingUserSubscription(caught.getMessage()));
}
});
}
@@ -111,13 +106,13 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
onCheckoutSuccessCallback, onCheckoutErrorCallback, onCheckoutCloseCallback));
;
} else {
view.onOpenCheckoutError("Failed to generating hosted page object, please try again");
view.onOpenCheckoutError(StringMessages.INSTANCE.failGeneratingHostedPageObject());
}
}
@Override
public void onFailure(Throwable caught) {
view.onOpenCheckoutError("Checkout error: " + caught.getMessage());
view.onOpenCheckoutError(StringMessages.INSTANCE.errorOpenCheckout(caught.getMessage()));
}
});
}
@@ -129,7 +124,7 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void onSuccess(Boolean result) {
if (!result) {
Window.alert("Failed to cancel the subscription");
showError(StringMessages.INSTANCE.failedCancelSubscription());
return;
}
@@ -138,12 +133,12 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void onFailure(Throwable caught) {
Window.alert("Cancel subscription error: " + caught.getMessage());
showError(StringMessages.INSTANCE.errorCancelSubscription(caught.getMessage()));
}
});
}
private void requestFinishingPlanUpgrading(String hostedPageId) {
private void requestFinishingPlanUpdating(String hostedPageId) {
clientFactory.getSubscriptionService().updatePlanSuccess(hostedPageId, new AsyncCallback<SubscriptionDTO>() {
@Override
@@ -155,9 +150,13 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void onFailure(Throwable caught) {
Chargebee.getInstance().closeAll();
Window.alert("Saving subscription data error: " + caught.getMessage());
showError(StringMessages.INSTANCE.errorSaveSubscription(caught.getMessage()));
}
});
}
private void showError(String message) {
Notification.notify(message, NotificationType.ERROR);
}
}
@@ -5,24 +5,24 @@ import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
public interface UserSubscriptionView extends IsWidget {
/**
* This is called on start loading subscription data
* Called on start loading subscription data
*/
public void onStartLoadSubscription();
/**
* Call to update the view with subscription data returned from backend
* Update the view with subscription data returned from back-end
*
* @param subscription
*/
public void updateView(SubscriptionDTO subscription);
/**
* Called on Chargebee checkout modal is closed
* Called on checkout modal is closed
*/
public void onCloseCheckoutModal();
/**
* Called on openning Chargebee checkout modal has errors
* Called on checkout modal has errors
*
* @param error
*/
@@ -35,7 +35,7 @@ public interface UserSubscriptionView extends IsWidget {
*/
public interface Presenter {
/**
* Init Chargebee
* Initialize
*/
public void init();
@@ -45,11 +45,10 @@ public interface UserSubscriptionView extends IsWidget {
public void loadSubscription();
/**
* Open Chargebee checkout modal from which user can create new subscription or change one if user is already in
* a subscription plan
* Open checkout modal from which user can create new subscription or change current subscription
*
* @param planId
* Id of plan user want to subscribe to
* Id of plan to subscribe to
*/
public void openCheckout(String planId);
@@ -2389,7 +2389,17 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages,
String openPortal();
String plans();
String inTrial();
String trialText(String endIn, String endAt);
String numHours(int hours);
String numDays(int days);
String numMinutes(int minutes);
String paymentStatusSuccess();
String paymentStatusNoSuccess();
String errorLoadingUserSubscription(String errMessage);
String failGeneratingHostedPageObject();
String errorOpenCheckout(String errMessage);
String failedCancelSubscription();
String errorCancelSubscription(String errMessage);
String errorSaveSubscription(String errMessage);
String unableToLoadCourseAreas(String message);
}
@@ -2395,4 +2395,14 @@ plans=Plans
inTrial=In Trial
paymentStatusSuccess=Success
paymentStatusNoSuccess=No Success
trialText=Your trial expires in {0} (ends on {1})
numHours={0} hour(s)
numDays={0} day(s)
numMinutes={0} minute(s)
errorLoadingUserSubscription=Error loading user subscription: {0}
failGeneratingHostedPageObject=Failed to generating hosted page object, please try again
errorOpenCheckout=Error open checkout form: {0}
failedCancelSubscription=Failed to cancel the subscription
errorCancelSubscription=Error canceling subscription: {0}
errorSaveSubscription=Error saving subscription: {0}
unableToLoadCourseAreas=Unable to load course areas: {0}
@@ -1,10 +1,8 @@
package com.sap.sailing.gwt.ui.server.subscription;
import java.io.Serializable;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -23,6 +21,7 @@ import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionPlans;
import com.sap.sse.security.SecurityService;
import com.sap.sse.security.shared.Subscription;
import com.sap.sse.security.shared.UserManagementException;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.ui.server.Activator;
@@ -37,7 +36,7 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
private static final Logger logger = Logger.getLogger(SubscriptionServiceImpl.class.getName());
private final BundleContext context;
private final FutureTask<SecurityService> securityService;
private final CompletableFuture<SecurityService> securityService;
public SubscriptionServiceImpl() {
// Configure payment service
@@ -47,30 +46,21 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
// get SecurityService
context = Activator.getContext();
final ServiceTracker<SecurityService, SecurityService> tracker = new ServiceTracker<>(context,
SecurityService.class, /* customizer */ null);
SecurityService.class, null);
tracker.open();
securityService = new FutureTask<SecurityService>(new Callable<SecurityService>() {
@Override
public SecurityService call() {
SecurityService result = null;
try {
logger.info("Waiting for SecurityService...");
result = tracker.waitForService(0);
logger.info("Obtained SecurityService " + result);
return result;
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Interrupted while waiting for UserStore service", e);
}
securityService = CompletableFuture.supplyAsync(() -> {
SecurityService result = null;
try {
logger.info("Waiting for SecurityService...");
result = tracker.waitForService(0);
logger.info("Obtained SecurityService " + result);
SecurityUtils.setSecurityManager(result.getSecurityManager());
return result;
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Interrupted while waiting for SecurityService service", e);
return null;
}
});
new Thread("ServiceTracker in bundle com.sap.sailing.gwt.ui.server.subscription for SecurityService") {
@Override
public void run() {
securityService.run();
SecurityUtils.setSecurityManager(getSecurityService().getSecurityManager());
}
}.start();
}
@Override
@@ -89,8 +79,8 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
// Check if user already subscribed to a plan, and if it's same plan with new plan then we stop the process
// and send back error
if (user.getSubscription() != null && user.getSubscription().planId != null
&& user.getSubscription().planId.equals(planId)) {
if (user.getSubscription() != null && user.getSubscription().getPlanId() != null
&& user.getSubscription().getPlanId().equals(planId)) {
response.error = "User has already subscribed to " + SubscriptionPlans.getPlan(planId).getName()
+ " plan";
return response;
@@ -99,7 +89,7 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
Result result;
// If there's no subscription data attach to user model then we create a checkout-new request
if (user.getSubscription() == null || user.getSubscription().planId == null) {
if (user.getSubscription() == null || user.getSubscription().getPlanId() == null) {
String[] userNameParts = user.getFullName().split("\\s+");
String firstName = userNameParts[0];
String lastName = "";
@@ -120,14 +110,12 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
} else {
// User has already subscribed to a plan, and user wants to change plan
// so we make a checkout-existing request
result = HostedPage.checkoutExisting().subscriptionId(user.getSubscription().subscriptionId)
result = HostedPage.checkoutExisting().subscriptionId(user.getSubscription().getSubscriptionId())
.subscriptionPlanId(planId).request();
}
response.hostedPageJSONString = result.hostedPage().toJson();
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in generating Chargebee hosted page data ", e);
response.error = "Error in generating Chargebee hosted page";
@@ -146,29 +134,28 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
Result result = HostedPage.acknowledge(hostedPageId).request();
Content content = result.hostedPage().content();
Subscription subscription = new Subscription();
subscription.subscriptionId = content.subscription().id();
subscription.customerId = content.customer().id();
subscription.planId = content.subscription().planId();
subscription.trialStart = Math.round(content.subscription().trialStart().getTime() / 1000);
subscription.trialEnd = Math.round(content.subscription().trialEnd().getTime() / 1000);
subscription.subscriptionStatus = content.subscription().status().name().toLowerCase();
subscription.subsciptionCreatedAt = Math.round(content.subscription().createdAt().getTime() / 1000);
subscription.subsciptionUpdatedAt = Math.round(content.subscription().updatedAt().getTime() / 1000);
subscription.latestEventTime = 0;
subscription.manualUpdatedAt = Math.round(System.currentTimeMillis() / 1000);
subscription.setSubscriptionId(content.subscription().id());
subscription.setCustomerId(content.customer().id());
subscription.setPlanId(content.subscription().planId());
subscription.setTrialStart(content.subscription().trialStart().getTime() / 1000);
subscription.setTrialEnd(content.subscription().trialEnd().getTime() / 1000);
subscription.setSubscriptionStatus(content.subscription().status().name().toLowerCase());
subscription.setSubsciptionCreatedAt(content.subscription().createdAt().getTime() / 1000);
subscription.setSubsciptionUpdatedAt(content.subscription().updatedAt().getTime() / 1000);
subscription.setLatestEventTime(0);
subscription.setManualUpdatedAt(System.currentTimeMillis() / 1000);
getSecurityService().updateUserSubscription(user.getName(), subscription);
subscriptionDto.planId = subscription.planId;
subscriptionDto.trialStart = subscription.trialStart;
subscriptionDto.trialEnd = subscription.trialEnd;
subscriptionDto.subscriptionStatus = subscription.subscriptionStatus;
subscriptionDto.paymentStatus = subscription.paymentStatus;
subscriptionDto.setPlanId(subscription.getPlanId());
subscriptionDto.setTrialStart(subscription.getTrialStart());
subscriptionDto.setTrialEnd(subscription.getTrialEnd());
subscriptionDto.setSubscriptionStatus(subscription.getSubscriptionStatus());
subscriptionDto.setPaymentStatus(subscription.getPaymentStatus());
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in saving subscription ", e);
subscriptionDto.error = e.getMessage();
subscriptionDto.setError(e.getMessage());
}
return subscriptionDto;
@@ -181,20 +168,19 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
User user = getCurrentUser();
Subscription subscription = user.getSubscription();
if (subscription == null || subscription.planId == null || subscription.planId.isEmpty()) {
if (subscription == null || subscription.getPlanId() == null || subscription.getPlanId().isEmpty()) {
return null;
}
subscriptionDto.planId = subscription.planId;
subscriptionDto.subscriptionStatus = subscription.subscriptionStatus;
subscriptionDto.paymentStatus = subscription.paymentStatus;
subscriptionDto.trialStart = subscription.trialStart;
subscriptionDto.trialEnd = subscription.trialEnd;
subscriptionDto.setPlanId(subscription.getPlanId());
subscriptionDto.setSubscriptionStatus(subscription.getSubscriptionStatus());
subscriptionDto.setPaymentStatus(subscription.getPaymentStatus());
subscriptionDto.setTrialStart(subscription.getTrialStart());
subscriptionDto.setTrialEnd(subscription.getTrialEnd());
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in getting subscription ", e);
subscriptionDto.error = e.getMessage();
subscriptionDto.setError(e.getMessage());
}
return subscriptionDto;
@@ -209,7 +195,7 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
return true;
}
String subscriptionId = subscription.subscriptionId;
String subscriptionId = subscription.getSubscriptionId();
if (subscriptionId != null && !subscriptionId.isEmpty()) {
// Send cancel request, verify result and only process if the result's subscription
// status is updated to be cancelled
@@ -222,12 +208,11 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
// we update user's subscription data with plan, subscription, status to be null
Subscription newSubscription = new Subscription();
newSubscription.latestEventTime = subscription.latestEventTime;
newSubscription.manualUpdatedAt = Math.round(System.currentTimeMillis() / 1000);
newSubscription.setLatestEventTime(subscription.getLatestEventTime());
newSubscription.setManualUpdatedAt(System.currentTimeMillis() / 1000);
getSecurityService().updateUserSubscription(user.getName(), newSubscription);
return true;
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in cancel subscription ", e);
return false;
}
@@ -241,29 +226,12 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
}
}
private User getCurrentUser() throws SubscriptionException {
private User getCurrentUser() throws UserManagementException {
User user = getSecurityService().getCurrentUser();
if (user == null) {
throw new SubscriptionException(SubscriptionException.INVALID_CURRENT_USER);
throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST);
}
return user;
}
private class SubscriptionException extends Exception implements Serializable {
private static final long serialVersionUID = 6321960099419330110L;
public static final String INVALID_CURRENT_USER = "Current user not found";
private final String message;
@Override
public String getMessage() {
return message;
}
public SubscriptionException(String message) {
this.message = message;
}
}
}
@@ -18,32 +18,32 @@ public class SubscriptionDTO implements IsSerializable {
/**
* User current subscription plan id
*/
public String planId;
private String planId;
/**
* Trial start timestamp
*/
public long trialStart;
private long trialStart;
/**
* Trial end timestamp
*/
public long trialEnd;
private long trialEnd;
/**
* Subscription status: active or in_trial
*/
public String subscriptionStatus;
private String subscriptionStatus;
/**
* Subscription payment status: success or no_success
*/
public String paymentStatus;
private String paymentStatus;
/**
* Error message
*/
public String error;
private String error;
/**
* Check if subscription is in trial status
@@ -51,7 +51,7 @@ public class SubscriptionDTO implements IsSerializable {
* @return
*/
public boolean isInTrial() {
return subscriptionStatus.equals(SUBSCRIPTION_STATUS_TRIAL);
return subscriptionStatus != null && subscriptionStatus.equals(SUBSCRIPTION_STATUS_TRIAL);
}
/**
@@ -60,11 +60,11 @@ public class SubscriptionDTO implements IsSerializable {
* @return
*/
public boolean isActive() {
return subscriptionStatus.equals(SUBSCRIPTION_STATUS_ACTIVE);
return subscriptionStatus != null && subscriptionStatus.equals(SUBSCRIPTION_STATUS_ACTIVE);
}
public boolean isPaymentSuccess() {
return paymentStatus.equals(PAYMENT_STATUS_SUCCESS);
return paymentStatus != null && paymentStatus.equals(PAYMENT_STATUS_SUCCESS);
}
/**
@@ -100,4 +100,52 @@ public class SubscriptionDTO implements IsSerializable {
return "";
}
public String getPlanId() {
return planId;
}
public void setPlanId(String planId) {
this.planId = planId;
}
public long getTrialStart() {
return trialStart;
}
public void setTrialStart(long trialStart) {
this.trialStart = trialStart;
}
public long getTrialEnd() {
return trialEnd;
}
public void setTrialEnd(long trialEnd) {
this.trialEnd = trialEnd;
}
public String getSubscriptionStatus() {
return subscriptionStatus;
}
public void setSubscriptionStatus(String subscriptionStatus) {
this.subscriptionStatus = subscriptionStatus;
}
public String getPaymentStatus() {
return paymentStatus;
}
public void setPaymentStatus(String paymentStatus) {
this.paymentStatus = paymentStatus;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
@@ -0,0 +1,22 @@
package com.sap.sailing.server.gateway.subscription;
public enum EventType {
CUSTOMER_DELETED("customer_deleted"),
SUBSCRIPTION_DELETED("subscription_deleted"),
SUBSCRIPTION_CREATED("subscription_created"),
SUBSCRIPTION_CHANGED("subscription_changed"),
SUBSCRIPTION_CANCELLED("subscription_cancelled"),
PAYMENT_SUCCEEDED("payment_succeeded"),
PAYMENT_FAILED("payment_failed"),
SUBSCRIPTION_ACTIVATED("subscription_activated");
private String name;
EventType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@@ -8,14 +8,6 @@ import org.json.simple.JSONObject;
* @author tutran
*/
public class SubscriptionWebhookEvent {
public static final String EVENT_CUSTOMER_DELETED = "customer_deleted";
public static final String EVENT_SUBSCRIPTION_DELETED = "subscription_deleted";
public static final String EVENT_SUBSCRIPTION_CREATED = "subscription_created";
public static final String EVENT_SUBSCRIPTION_CHANGED = "subscription_changed";
public static final String EVENT_SUBSCRIPTION_CANCELLED = "subscription_cancelled";
public static final String EVENT_PAYMENT_SUCCEEDED = "payment_succeeded";
public static final String EVENT_PAYMENT_FAILED = "payment_failed";
public static final String EVENT_SUBSCRIPTION_ACTIVATED = "subscription_activated";
public static final String SUBSCRIPTION_STATUS_ACTIVE = "active";
public static final String INVOICE_STATUS_PAID = "paid";
@@ -43,8 +35,12 @@ public class SubscriptionWebhookEvent {
return eventId;
}
public String getEventType() {
return eventType;
public EventType getEventType() {
try {
return EventType.valueOf(eventType.toUpperCase());
} catch (Exception e) {
return null;
}
}
public String getCustomerEmail() {
@@ -7,6 +7,7 @@ import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
@@ -32,7 +33,7 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
Object requestBody = JSONValue.parseWithException(request.getReader());
JSONObject requestObject = Helpers.toJSONObjectSafe(requestBody);
@@ -45,7 +46,7 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
User user = getUser(event.getCustomerId());
if (user == null) {
response.setStatus(200);
sendSuccess(response);
return;
}
@@ -55,75 +56,79 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
// Verify the event time, only process if event time is larger than last success processed event and last
// updated time by user
if (userSubscription != null && occuredAt < user.getSubscription().latestEventTime
&& occuredAt < user.getSubscription().manualUpdatedAt) {
response.setStatus(200);
if (userSubscription != null && occuredAt < user.getSubscription().getLatestEventTime()
&& occuredAt < user.getSubscription().getManualUpdatedAt()) {
sendSuccess(response);
return;
}
switch (event.getEventType()) {
case SubscriptionWebhookEvent.EVENT_CUSTOMER_DELETED:
EventType eventType = event.getEventType();
if (eventType == null) {
sendSuccess(response);
return;
}
switch (eventType) {
case CUSTOMER_DELETED:
updateUserSubscription(user, buildEmptySubscription(userSubscription, event));
break;
case SubscriptionWebhookEvent.EVENT_SUBSCRIPTION_CANCELLED:
case SubscriptionWebhookEvent.EVENT_SUBSCRIPTION_DELETED:
if (userSubscription != null && userSubscription.subscriptionId != null
&& userSubscription.subscriptionId.equals(event.getSubscriptionId())) {
case SUBSCRIPTION_CANCELLED:
case SUBSCRIPTION_DELETED:
if (userSubscription != null && userSubscription.getSubscriptionId() != null
&& userSubscription.getSubscriptionId().equals(event.getSubscriptionId())) {
updateUserSubscription(user, buildEmptySubscription(userSubscription, event));
}
break;
case SubscriptionWebhookEvent.EVENT_SUBSCRIPTION_CREATED:
case SubscriptionWebhookEvent.EVENT_SUBSCRIPTION_CHANGED:
case SubscriptionWebhookEvent.EVENT_SUBSCRIPTION_ACTIVATED:
case SubscriptionWebhookEvent.EVENT_PAYMENT_SUCCEEDED:
case SubscriptionWebhookEvent.EVENT_PAYMENT_FAILED:
case SUBSCRIPTION_CREATED:
case SUBSCRIPTION_CHANGED:
case SUBSCRIPTION_ACTIVATED:
case PAYMENT_SUCCEEDED:
case PAYMENT_FAILED:
updateUserSubscription(user, buildSubscription(userSubscription, event));
break;
}
response.setStatus(200);
sendSuccess(response);
} catch (ParseException e) {
e.printStackTrace();
logger.log(Level.WARNING, "Failed to parse subscription webhook event data: " + e.getMessage());
response.setStatus(500);
sendFail(response);
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Failed to proccess subscription webhook event: " + e.getMessage());
response.setStatus(500);
sendFail(response);
}
}
private Subscription buildEmptySubscription(Subscription currentSubscription, SubscriptionWebhookEvent event) {
Subscription subscription = new Subscription();
subscription.latestEventTime = event.getEventOccurredAt();
subscription.manualUpdatedAt = currentSubscription != null ? currentSubscription.manualUpdatedAt : 0;
subscription.setLatestEventTime(event.getEventOccurredAt());
subscription.setManualUpdatedAt(currentSubscription != null ? currentSubscription.getManualUpdatedAt() : 0);
return subscription;
}
private Subscription buildSubscription(Subscription currentSubscription, SubscriptionWebhookEvent event) {
Subscription subscription = new Subscription();
subscription.subscriptionId = event.getSubscriptionId();
subscription.planId = event.getPlanId();
subscription.customerId = event.getCustomerId();
subscription.trialStart = event.getSubscriptionTrialStart();
subscription.trialEnd = event.getSubscriptionTrialEnd();
subscription.subscriptionStatus = event.getSubscriptionStatus();
subscription.subsciptionCreatedAt = event.getSubscriptionCreatedAt();
subscription.subsciptionUpdatedAt = event.getSubscriptionUpdatedAt();
subscription.latestEventTime = event.getEventOccurredAt();
subscription.manualUpdatedAt = currentSubscription != null ? currentSubscription.manualUpdatedAt : 0;
subscription.setSubscriptionId(event.getSubscriptionId());
subscription.setPlanId(event.getPlanId());
subscription.setCustomerId(event.getCustomerId());
subscription.setTrialStart(event.getSubscriptionTrialStart());
subscription.setTrialEnd(event.getSubscriptionTrialEnd());
subscription.setSubscriptionStatus(event.getSubscriptionStatus());
subscription.setSubsciptionCreatedAt(event.getSubscriptionCreatedAt());
subscription.setSubsciptionUpdatedAt(event.getSubscriptionUpdatedAt());
subscription.setLatestEventTime(event.getEventOccurredAt());
subscription.setManualUpdatedAt(currentSubscription != null ? currentSubscription.getManualUpdatedAt() : 0);
if (subscription.subscriptionStatus != null
&& subscription.subscriptionStatus.equals(SubscriptionWebhookEvent.SUBSCRIPTION_STATUS_ACTIVE)) {
if (subscription.getSubscriptionStatus() != null
&& subscription.getSubscriptionStatus().equals(SubscriptionWebhookEvent.SUBSCRIPTION_STATUS_ACTIVE)) {
String paymentStatus = getEventPaymentStatus(event);
if (paymentStatus != null) {
subscription.paymentStatus = paymentStatus;
subscription.setPaymentStatus(paymentStatus);
} else {
subscription.paymentStatus = currentSubscription != null ? currentSubscription.paymentStatus : null;
subscription.setPaymentStatus(currentSubscription != null ? currentSubscription.getPaymentStatus() : null);
}
} else {
subscription.paymentStatus = null;
subscription.setPaymentStatus(null);
}
return subscription;
@@ -158,4 +163,12 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
return paymentStatus;
}
private void sendSuccess(HttpServletResponse response) {
response.setStatus(Response.Status.OK.getStatusCode());
}
private void sendFail(HttpServletResponse response) {
response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
}
}
@@ -19,27 +19,27 @@ public class Subscription implements Serializable {
/**
* Subscription id from Chargebee
*/
public String subscriptionId;
private String subscriptionId;
/**
* Current subscription plan id
*/
public String planId;
private String planId;
/**
* Chargebee's customer id, this is same as the system's user name
*/
public String customerId;
private String customerId;
/**
* Subscription trial start timestamp
*/
public long trialStart;
private long trialStart;
/**
* Subscription trial end timestamp
*/
public long trialEnd;
private long trialEnd;
/**
* Subscription status, it could be in_trial, active, or cancelled.
@@ -47,24 +47,24 @@ public class Subscription implements Serializable {
* active means the subscription is active
* cancelled means the subscription has been cancelled(by user or by admin from Chargebee dashboard)
*/
public String subscriptionStatus;
private String subscriptionStatus;
/**
* Subscription payment status, it records if user has successfully paid for the subscription.
* User will pay for the subscription only if the subscription is turned to active(after trial period)
* If user has successfully paid for the subscription, this has value "success", otherwise "no_success"
*/
public String paymentStatus;
private String paymentStatus;
/**
* Record the creating timestamp of the Chargebee's subscription
*/
public long subsciptionCreatedAt;
private long subsciptionCreatedAt;
/**
* Record the updating timestamp of the Chargebee's subscription
*/
public long subsciptionUpdatedAt;
private long subsciptionUpdatedAt;
/**
* Record the timestamp of the latest handled webhook event.
@@ -73,12 +73,100 @@ public class Subscription implements Serializable {
*
* {@link https://www.chargebee.com/docs/webhook_settings.html#automatic-retries}
*/
public long latestEventTime;
private long latestEventTime;
/**
* Record the timestamp the subscription was updated by user using the system, like changing plan or cancel subscription.
* Reason is Chargebee will retried to send us failed webhook events, so with this timestamp we'll process only webhook events
* occur after this timestamp, otherwise we'll update with old data.
*/
public long manualUpdatedAt;
private long manualUpdatedAt;
public String getSubscriptionId() {
return subscriptionId;
}
public void setSubscriptionId(String subscriptionId) {
this.subscriptionId = subscriptionId;
}
public String getPlanId() {
return planId;
}
public void setPlanId(String planId) {
this.planId = planId;
}
public String getCustomerId() {
return customerId;
}
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
public long getTrialStart() {
return trialStart;
}
public void setTrialStart(long trialStart) {
this.trialStart = trialStart;
}
public long getTrialEnd() {
return trialEnd;
}
public void setTrialEnd(long trialEnd) {
this.trialEnd = trialEnd;
}
public String getSubscriptionStatus() {
return subscriptionStatus;
}
public void setSubscriptionStatus(String subscriptionStatus) {
this.subscriptionStatus = subscriptionStatus;
}
public String getPaymentStatus() {
return paymentStatus;
}
public void setPaymentStatus(String paymentStatus) {
this.paymentStatus = paymentStatus;
}
public long getSubsciptionCreatedAt() {
return subsciptionCreatedAt;
}
public void setSubsciptionCreatedAt(long subsciptionCreatedAt) {
this.subsciptionCreatedAt = subsciptionCreatedAt;
}
public long getSubsciptionUpdatedAt() {
return subsciptionUpdatedAt;
}
public void setSubsciptionUpdatedAt(long subsciptionUpdatedAt) {
this.subsciptionUpdatedAt = subsciptionUpdatedAt;
}
public long getLatestEventTime() {
return latestEventTime;
}
public void setLatestEventTime(long latestEventTime) {
this.latestEventTime = latestEventTime;
}
public long getManualUpdatedAt() {
return manualUpdatedAt;
}
public void setManualUpdatedAt(long manualUpdatedAt) {
this.manualUpdatedAt = manualUpdatedAt;
}
}
@@ -497,16 +497,16 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
}
Subscription subscription = new Subscription();
subscription.customerId = subscriptionDoc.getString(FieldNames.Subscription.CUSTOMER_ID.name());
subscription.planId = subscriptionDoc.getString(FieldNames.Subscription.PLAN_ID.name());
subscription.subscriptionStatus = subscriptionDoc.getString(FieldNames.Subscription.SUBSCRIPTION_STATUS.name());
subscription.subscriptionId = subscriptionDoc.getString(FieldNames.Subscription.SUBSCRIPTION_ID.name());
subscription.trialStart = subscriptionDoc.getLong(FieldNames.Subscription.TRIAL_START.name());
subscription.trialEnd = subscriptionDoc.getLong(FieldNames.Subscription.TRIAL_END.name());
subscription.paymentStatus = subscriptionDoc.getString(FieldNames.Subscription.PAYMENT_STATUS.name());
subscription.subsciptionCreatedAt = subscriptionDoc.getLong(FieldNames.Subscription.SUBSCRIPTION_CREATED_AT.name());
subscription.subsciptionUpdatedAt = subscriptionDoc.getLong(FieldNames.Subscription.SUBSCRIPTION_UPDATED_AT.name());
subscription.latestEventTime = subscriptionDoc.getLong(FieldNames.Subscription.LATEST_EVENT_TIME.name());
subscription.setCustomerId(subscriptionDoc.getString(FieldNames.Subscription.CUSTOMER_ID.name()));
subscription.setPlanId(subscriptionDoc.getString(FieldNames.Subscription.PLAN_ID.name()));
subscription.setSubscriptionStatus(subscriptionDoc.getString(FieldNames.Subscription.SUBSCRIPTION_STATUS.name()));
subscription.setSubscriptionId(subscriptionDoc.getString(FieldNames.Subscription.SUBSCRIPTION_ID.name()));
subscription.setTrialStart(subscriptionDoc.getLong(FieldNames.Subscription.TRIAL_START.name()));
subscription.setTrialEnd(subscriptionDoc.getLong(FieldNames.Subscription.TRIAL_END.name()));
subscription.setPaymentStatus(subscriptionDoc.getString(FieldNames.Subscription.PAYMENT_STATUS.name()));
subscription.setSubsciptionCreatedAt(subscriptionDoc.getLong(FieldNames.Subscription.SUBSCRIPTION_CREATED_AT.name()));
subscription.setSubsciptionUpdatedAt(subscriptionDoc.getLong(FieldNames.Subscription.SUBSCRIPTION_UPDATED_AT.name()));
subscription.setLatestEventTime(subscriptionDoc.getLong(FieldNames.Subscription.LATEST_EVENT_TIME.name()));
return subscription;
}
@@ -312,17 +312,17 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
}
Document doc = new Document();
doc.put(FieldNames.Subscription.SUBSCRIPTION_ID.name(), subscription.subscriptionId);
doc.put(FieldNames.Subscription.PLAN_ID.name(), subscription.planId);
doc.put(FieldNames.Subscription.CUSTOMER_ID.name(), subscription.customerId);
doc.put(FieldNames.Subscription.TRIAL_START.name(), subscription.trialStart);
doc.put(FieldNames.Subscription.TRIAL_END.name(), subscription.trialEnd);
doc.put(FieldNames.Subscription.SUBSCRIPTION_STATUS.name(), subscription.subscriptionStatus);
doc.put(FieldNames.Subscription.PAYMENT_STATUS.name(), subscription.paymentStatus);
doc.put(FieldNames.Subscription.SUBSCRIPTION_CREATED_AT.name(), subscription.subsciptionCreatedAt);
doc.put(FieldNames.Subscription.SUBSCRIPTION_UPDATED_AT.name(), subscription.subsciptionUpdatedAt);
doc.put(FieldNames.Subscription.LATEST_EVENT_TIME.name(), subscription.latestEventTime);
doc.put(FieldNames.Subscription.MANUAL_UPDATED_AT.name(), subscription.manualUpdatedAt);
doc.put(FieldNames.Subscription.SUBSCRIPTION_ID.name(), subscription.getSubscriptionId());
doc.put(FieldNames.Subscription.PLAN_ID.name(), subscription.getPlanId());
doc.put(FieldNames.Subscription.CUSTOMER_ID.name(), subscription.getCustomerId());
doc.put(FieldNames.Subscription.TRIAL_START.name(), subscription.getTrialStart());
doc.put(FieldNames.Subscription.TRIAL_END.name(), subscription.getTrialEnd());
doc.put(FieldNames.Subscription.SUBSCRIPTION_STATUS.name(), subscription.getSubscriptionStatus());
doc.put(FieldNames.Subscription.PAYMENT_STATUS.name(), subscription.getPaymentStatus());
doc.put(FieldNames.Subscription.SUBSCRIPTION_CREATED_AT.name(), subscription.getSubsciptionCreatedAt());
doc.put(FieldNames.Subscription.SUBSCRIPTION_UPDATED_AT.name(), subscription.getSubsciptionUpdatedAt());
doc.put(FieldNames.Subscription.LATEST_EVENT_TIME.name(), subscription.getLatestEventTime());
doc.put(FieldNames.Subscription.MANUAL_UPDATED_AT.name(), subscription.getManualUpdatedAt());
return doc;
}