Payment: add comments, format the code

This commit is contained in:
Tu Tran
2020-05-19 11:26:17 +07:00
parent 440f584ddf
commit 5ae0f226d1
27 changed files with 599 additions and 367 deletions
@@ -5,19 +5,27 @@ import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
public interface SubscriptionProfileDesktopResources extends ClientBundle {
public static final SubscriptionProfileDesktopResources INSTANCE = GWT.create(SubscriptionProfileDesktopResources.class);
public static final SubscriptionProfileDesktopResources INSTANCE = GWT
.create(SubscriptionProfileDesktopResources.class);
@Source("SubscriptionProfiles.gss")
SubscriptionProfileCss css();
public interface SubscriptionProfileCss extends CssResource {
String bottomButton();
String trialText();
String textRow();
String errorText();
String blueText();
String plansLabel();
String plansInput();
String planInputContainer();
}
}
@@ -8,18 +8,18 @@ import com.sap.sailing.gwt.home.shared.places.user.profile.subscription.UserSubs
import com.sap.sse.security.ui.authentication.app.AuthenticationContext;
public class UserProfileSubscriptionPresenter implements UserProfileSubscriptionView.Presenter {
private final UserProfileSubscriptionView view;
private final UserProfileView.Presenter userProfilePresenter;
private final UserSubscriptionView.Presenter userSubscriptionPresenter;
public UserProfileSubscriptionPresenter(final UserProfileSubscriptionView view,
final UserProfileView.Presenter userProfilePresenter) {
this.view = view;
this.userProfilePresenter = userProfilePresenter;
this.userSubscriptionPresenter = new UserSubscriptionPresenter<UserProfileClientFactory>(
userProfilePresenter.getClientFactory());
view.setPresenter(this);
this.view = view;
this.userProfilePresenter = userProfilePresenter;
this.userSubscriptionPresenter = new UserSubscriptionPresenter<UserProfileClientFactory>(
userProfilePresenter.getClientFactory());
view.setPresenter(this);
}
@Override
@@ -9,14 +9,14 @@ import com.sap.sailing.gwt.home.shared.ExperimentalFeatures;
import com.sap.sailing.gwt.home.shared.places.user.profile.subscription.UserProfileSubscriptionPlace;
import com.sap.sse.security.ui.authentication.app.AuthenticationContext;
public class UserProfileSubscriptionTabView extends Composite implements UserProfileTabView<UserProfileSubscriptionPlace> {
public class UserProfileSubscriptionTabView extends Composite
implements UserProfileTabView<UserProfileSubscriptionPlace> {
private UserProfileSubscriptionView view;
private UserProfileSubscriptionView.Presenter currentPresenter;
@Override
public Class<UserProfileSubscriptionPlace> getPlaceClassForActivation() {
// TODO Auto-generated method stub
return UserProfileSubscriptionPlace.class;
}
@@ -8,9 +8,9 @@ import com.sap.sse.security.ui.authentication.decorator.NotLoggedInPresenter;
public interface UserProfileSubscriptionView extends IsWidget {
void setPresenter(Presenter presenter);
NeedsAuthenticationContext getDecorator();
public interface Presenter extends NotLoggedInPresenter, NeedsAuthenticationContext {
UserSubscriptionView.Presenter getUserSubscriptionPresenter();
}
@@ -14,9 +14,11 @@ public class UserProfileSubscriptionViewImpl extends Composite implements UserPr
}
private static MyBinder uiBinder = GWT.create(MyBinder.class);
@UiField(provided = true) AuthorizedContentDecoratorDesktop decoratorUi;
@UiField(provided = true) UserSubscription userSubscriptionUi;
@UiField(provided = true)
AuthorizedContentDecoratorDesktop decoratorUi;
@UiField(provided = true)
UserSubscription userSubscriptionUi;
@Override
public void setPresenter(Presenter presenter) {
@@ -24,7 +26,7 @@ public class UserProfileSubscriptionViewImpl extends Composite implements UserPr
userSubscriptionUi = new UserSubscription(presenter.getUserSubscriptionPresenter());
initWidget(uiBinder.createAndBindUi(this));
}
@Override
protected void onLoad() {
super.onLoad();
@@ -25,64 +25,76 @@ import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionPlans;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionPlans.Plan;
/**
* View for displaying user subscription information like plan, subscription status...
* In this view user is able to subscribe to a plan or cancel current subscription.
* View for displaying user subscription information like plan, subscription status...In this view user is able to
* subscribe to a plan or cancel current subscription.
*
* @author tutran
*/
public class UserSubscription extends Composite implements UserSubscriptionView {
interface MyUiBinder extends UiBinder<Widget, UserSubscription> {}
interface MyUiBinder extends UiBinder<Widget, UserSubscription> {
}
private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
@UiField DivElement rootUi;
@UiField Button updateSubscriptionButtonUi;
@UiField Button cancelSubscriptionButtonUi;
@UiField SpanElement planNameSpanUi;
@UiField DivElement trialDivUi;
@UiField DivElement subscriptionGroupUi;
@UiField SpanElement subscriptionStatusSpanUi;
@UiField SpanElement paymentStatusSpanUi;
@UiField DivElement paymentStatusDivUi;
@UiField ListBox planListUi;
@UiField
DivElement rootUi;
@UiField
Button updateSubscriptionButtonUi;
@UiField
Button cancelSubscriptionButtonUi;
@UiField
SpanElement planNameSpanUi;
@UiField
DivElement trialDivUi;
@UiField
DivElement subscriptionGroupUi;
@UiField
SpanElement subscriptionStatusSpanUi;
@UiField
SpanElement paymentStatusSpanUi;
@UiField
DivElement paymentStatusDivUi;
@UiField
ListBox planListUi;
private Presenter presenter;
public UserSubscription(UserSubscriptionView.Presenter presenter) {
initWidget(uiBinder.createAndBindUi(this));
presenter.setView(this);
this.presenter = presenter;
}
@UiHandler("updateSubscriptionButtonUi")
public void handleUpdateSubscriptionClick(ClickEvent e) {
updateSubscriptionButtonUi.setEnabled(false);
presenter.openCheckout(planListUi.getSelectedValue());
}
@UiHandler("cancelSubscriptionButtonUi")
public void handleCancelSubscriptionClick(ClickEvent e) {
cancelSubscriptionButtonUi.setEnabled(false);
presenter.cancelSubscription();
}
@Override
protected void onLoad() {
super.onLoad();
SubscriptionProfileDesktopResources.INSTANCE.css().ensureInjected();
}
@Override
public void onStartLoadSubscription() {
hide();
}
@Override
public void onOpenCheckoutError(String error) {
updateSubscriptionButtonUi.setEnabled(true);
Window.alert(error);
}
@Override
public void onCloseCheckoutModal() {
updateSubscriptionButtonUi.setEnabled(true);
@@ -91,16 +103,16 @@ public class UserSubscription extends Composite implements UserSubscriptionView
@Override
public void updateView(SubscriptionDTO subscription) {
resetElementsVisibleState();
updatePlanList(subscription);
updateSubscriptionButtonUi.setEnabled(true);
Plan plan = null;
if (subscription != null) {
plan = SubscriptionPlans.getPlan(subscription.planId);
}
if (subscription == null || plan == null) {
planNameSpanUi.setInnerText("Free");
hideElement(subscriptionGroupUi);
@@ -108,33 +120,32 @@ public class UserSubscription extends Composite implements UserSubscriptionView
show();
return;
}
planNameSpanUi.setInnerText(plan.getName());
subscriptionStatusSpanUi.setInnerText(subscription.getSubscriptionStatusLabel());
if (subscription.isActive()) {
subscriptionStatusSpanUi.addClassName(SubscriptionProfileDesktopResources.INSTANCE.css().blueText());
paymentStatusSpanUi.setInnerText(subscription.getPaymentStatusLabel());
paymentStatusSpanUi.addClassName(
subscription.isPaymentSuccess() ?
SubscriptionProfileDesktopResources.INSTANCE.css().blueText() :
SubscriptionProfileDesktopResources.INSTANCE.css().errorText());
subscription.isPaymentSuccess() ? SubscriptionProfileDesktopResources.INSTANCE.css().blueText()
: SubscriptionProfileDesktopResources.INSTANCE.css().errorText());
} else {
hideElement(paymentStatusDivUi);
if (subscription.isInTrial()) {
trialDivUi.setInnerText(buildTrialText(subscription));
} else {
hideElement(trialDivUi);
}
}
show();
}
private void updatePlanList(SubscriptionDTO subscription) {
planListUi.clear();
if (subscription == null) {
planListUi.addItem("", "-- Select plan --");
planListUi.addItem("", "");
planListUi.setSelectedIndex(0);
}
List<Plan> planList = SubscriptionPlans.getPlanList();
@@ -146,7 +157,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
}
}
}
private String buildTrialText(SubscriptionDTO subscription) {
long now = Math.round(Duration.currentTimeMillis() / 1000);
long remain = subscription.trialEnd - now;
@@ -162,7 +173,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
}
}
remain = remain % 86400;
int hours = (int)(remain / 3600);
int hours = (int) (remain / 3600);
if (hours > 0) {
if (remainText.length() > 0) {
remainText.append(" ");
@@ -174,7 +185,7 @@ public class UserSubscription extends Composite implements UserSubscriptionView
}
if (days == 0) {
remain = remain % 3600;
int mins = (int)(remain / 60);
int mins = (int) (remain / 60);
if (mins > 0) {
if (remainText.length() > 0) {
remainText.append(" ");
@@ -186,40 +197,38 @@ public class UserSubscription extends Composite implements UserSubscriptionView
}
}
}
if (remainText.length() == 0) {
remainText.append("0 day");
}
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(")");
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();
}
private void resetElementsVisibleState() {
showElement(subscriptionGroupUi);
cancelSubscriptionButtonUi.setVisible(true);
showElement(paymentStatusDivUi);
showElement(trialDivUi);
}
private void hideElement(Element element) {
element.getStyle().setDisplay(Display.NONE);
}
private void showElement(Element element) {
element.getStyle().setDisplay(Display.BLOCK);
}
private void hide() {
rootUi.getStyle().setDisplay(Display.NONE);
}
private void show() {
rootUi.getStyle().setDisplay(Display.BLOCK);
}
@@ -14,82 +14,73 @@ 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
* 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 {
implements UserSubscriptionView.Presenter {
private final C clientFactory;
private UserSubscriptionView view;
/**
* Callback for Chargebee checkout success event
*/
private CheckoutOption.SuccessCallback onCheckoutSuccessCallback =
new CheckoutOption.SuccessCallback() {
@Override
public void call(String hostedPageId) {
requestFinishingPlanUpgrading(hostedPageId);
}
};
private CheckoutOption.SuccessCallback onCheckoutSuccessCallback = new CheckoutOption.SuccessCallback() {
@Override
public void call(String hostedPageId) {
requestFinishingPlanUpgrading(hostedPageId);
}
};
/**
* Callback for Chargebee checkout fail event
*/
private CheckoutOption.ErrorCallback onCheckoutErrorCallback =
new CheckoutOption.ErrorCallback() {
@Override
public void call(String error) {
view.onOpenCheckoutError(error);
}
};
private CheckoutOption.ErrorCallback onCheckoutErrorCallback = new CheckoutOption.ErrorCallback() {
@Override
public void call(String error) {
view.onOpenCheckoutError(error);
}
};
/**
* Callback for Chargebee checkout modal close event
*/
private CheckoutOption.CloseCallback onCheckoutCloseCallback =
new CheckoutOption.CloseCallback() {
@Override
public void call() {
view.onCloseCheckoutModal();
}
};
private CheckoutOption.CloseCallback onCheckoutCloseCallback = new CheckoutOption.CloseCallback() {
@Override
public void call() {
view.onCloseCheckoutModal();
}
};
public UserSubscriptionPresenter(C clientFactory) {
this.clientFactory = clientFactory;
}
/**
* Init Chargebee
*/
@Override
public void init() {
Chargebee.init(Chargebee.InitOption.create(SubscriptionConfiguration.CHARGEBEE_SITE));
}
/**
* Load user's subscription data
*/
@Override
public void loadSubscription() {
view.onStartLoadSubscription();
clientFactory.getSubscriptionService().getSubscription(new AsyncCallback<SubscriptionDTO>() {
@Override
public void onSuccess(SubscriptionDTO result) {
if (result != null && result.error != null && !result.error.isEmpty()) {
Window.alert("Get user subscription error: " + result.error);
return;
}
view.updateView(result);
}
@@ -102,56 +93,46 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
@Override
public void setView(UserSubscriptionView view) {
this.view = view;
this.view = view;
}
/**
* Open Chargebee checkout modal from which user can create new subscription or change one if user is already in a subscription plan
*
* @param planId Id of plan user want to subscribe to
*/
@Override
public void openCheckout(String planId) {
clientFactory.getSubscriptionService().generateHostedPageObject(planId, new AsyncCallback<HostedPageResultDTO>() {
@Override
public void onSuccess(HostedPageResultDTO hostedPage) {
if (hostedPage.error != null && !hostedPage.error.isEmpty()) {
view.onOpenCheckoutError(hostedPage.error);
} else if (hostedPage.hostedPageJSONString != null && !hostedPage.hostedPageJSONString.isEmpty()) {
Chargebee.getInstance().openCheckout(
CheckoutOption.create(
hostedPage.hostedPageJSONString,
onCheckoutSuccessCallback,
onCheckoutErrorCallback,
onCheckoutCloseCallback
));;
} else {
view.onOpenCheckoutError("Failed to generating hosted page object, please try again");
}
}
@Override
public void onFailure(Throwable caught) {
view.onOpenCheckoutError("Checkout error: " + caught.getMessage());
}
});
clientFactory.getSubscriptionService().generateHostedPageObject(planId,
new AsyncCallback<HostedPageResultDTO>() {
@Override
public void onSuccess(HostedPageResultDTO hostedPage) {
if (hostedPage.error != null && !hostedPage.error.isEmpty()) {
view.onOpenCheckoutError(hostedPage.error);
} else if (hostedPage.hostedPageJSONString != null
&& !hostedPage.hostedPageJSONString.isEmpty()) {
Chargebee.getInstance().openCheckout(CheckoutOption.create(hostedPage.hostedPageJSONString,
onCheckoutSuccessCallback, onCheckoutErrorCallback, onCheckoutCloseCallback));
;
} else {
view.onOpenCheckoutError("Failed to generating hosted page object, please try again");
}
}
@Override
public void onFailure(Throwable caught) {
view.onOpenCheckoutError("Checkout error: " + caught.getMessage());
}
});
}
/**
* Cancel current user's subscription
*/
@Override
public void cancelSubscription() {
clientFactory.getSubscriptionService().cancelSubscription(new AsyncCallback<Boolean>() {
@Override
public void onSuccess(Boolean result) {
if (!result) {
Window.alert("Failed to cancel the subscription");
return;
}
view.updateView(null);
}
@@ -161,7 +142,7 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
}
});
}
private void requestFinishingPlanUpgrading(String hostedPageId) {
clientFactory.getSubscriptionService().updatePlanSuccess(hostedPageId, new AsyncCallback<SubscriptionDTO>() {
@@ -170,13 +151,13 @@ public class UserSubscriptionPresenter<C extends ClientFactoryWithDispatch & Err
view.updateView(result);
Chargebee.getInstance().closeAll();
}
@Override
public void onFailure(Throwable caught) {
Chargebee.getInstance().closeAll();
Window.alert("Saving subscription data error: " + caught.getMessage());
}
});
}
}
@@ -4,16 +4,60 @@ import com.google.gwt.user.client.ui.IsWidget;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
public interface UserSubscriptionView extends IsWidget {
/**
* This is called on start loading subscription data
*/
public void onStartLoadSubscription();
/**
* Call to update the view with subscription data returned from backend
*
* @param subscription
*/
public void updateView(SubscriptionDTO subscription);
/**
* Called on Chargebee checkout modal is closed
*/
public void onCloseCheckoutModal();
/**
* Called on openning Chargebee checkout modal has errors
*
* @param error
*/
public void onOpenCheckoutError(String error);
/**
* Presenter for {@link UserSubscriptionView}
*
* @author tutran
*/
public interface Presenter {
/**
* Init Chargebee
*/
public void init();
/**
* Load user's subscription data
*/
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
*
* @param planId
* Id of plan user want to subscribe to
*/
public void openCheckout(String planId);
public void setView(UserSubscriptionView view);
/**
* Request to cancel current user's subscription
*/
public void cancelSubscription();
}
}
@@ -14,24 +14,27 @@ import jsinterop.annotations.JsType;
public class Chargebee {
/**
* Init Chargebee module {@link https://www.chargebee.com/checkout-portal-docs/api.html#chargebee-object}
*
* @param options
* @return
*/
public static native ChargebeeInstance init(InitOption options);
/**
* Get Chargebee instance which is available only after initialization
* {@link https://www.chargebee.com/checkout-portal-docs/api.html#getinstance}
*
* @return
*/
public static native ChargebeeInstance getInstance();
public static class InitOption extends JavaScriptObject {
protected InitOption() {}
protected InitOption() {
}
public static native InitOption create(String site) /*-{
return {
site: site
site : site
};
}-*/;
}
@@ -11,24 +11,29 @@ import jsinterop.annotations.JsType;
public class ChargebeeInstance {
/**
* Open Chargebee checkout modal {@link https://www.chargebee.com/checkout-portal-docs/api.html#opencheckout}
*
* @param option
*/
public native void openCheckout(CheckoutOption option);
/**
* Close Chargebee checkout modal {@link https://www.chargebee.com/checkout-portal-docs/api.html#closeall}
*/
public native void closeAll();
/**
* Set Chargebee customer portal session {@link https://www.chargebee.com/checkout-portal-docs/api.html#setportalsession}
* Use this in case we want integrate customer portal in the application
* Set Chargebee customer portal session
* {@link https://www.chargebee.com/checkout-portal-docs/api.html#setportalsession} Use this in case we want
* integrate customer portal in the application
*
* @param sessionSetter
*/
public native void setPortalSession(PortalSessionSetterCallback sessionSetter);
/**
* Create Chargebee customer portal instance {@link https://www.chargebee.com/checkout-portal-docs/api.html#createchargebeeportal}
* Create Chargebee customer portal instance
* {@link https://www.chargebee.com/checkout-portal-docs/api.html#createchargebeeportal}
*
* @return
*/
public native ChargebeePortal createChargebeePortal();
@@ -3,39 +3,40 @@ package com.sap.sailing.gwt.ui.client.subscription;
import com.google.gwt.core.client.JavaScriptObject;
/**
* Class represent JS option object for Chargebee instance openCheckout method
* {@link ChargebeeInstance}
* Class represent JS option object for Chargebee instance openCheckout method {@link ChargebeeInstance}
*
* @author tutran
*/
public class CheckoutOption extends JavaScriptObject {
protected CheckoutOption() {}
public static native CheckoutOption create(String hostedPage, SuccessCallback onSuccess, ErrorCallback onError, CloseCallback onClose) /*-{
protected CheckoutOption() {
}
public static native CheckoutOption create(String hostedPage, SuccessCallback onSuccess, ErrorCallback onError,
CloseCallback onClose) /*-{
return {
hostedPage: function() {
hostedPage : function() {
return Promise.resolve(JSON.parse(hostedPage));
},
success: function(hostedPageId) {
success : function(hostedPageId) {
onSuccess.@com.sap.sailing.gwt.ui.client.subscription.CheckoutOption.SuccessCallback::call(Ljava/lang/String;)(hostedPageId);
},
error: function(error) {
error : function(error) {
onError.@com.sap.sailing.gwt.ui.client.subscription.CheckoutOption.ErrorCallback::call(Ljava/lang/String;)(error.message ? error.message : error);
},
close: function() {
close : function() {
onClose.@com.sap.sailing.gwt.ui.client.subscription.CheckoutOption.CloseCallback::call()();
}
};
}-*/;
public static interface SuccessCallback {
void call(String hostedPageId);
}
public static interface ErrorCallback {
void call(String error);
}
public static abstract interface CloseCallback {
void call();
}
@@ -3,22 +3,22 @@ package com.sap.sailing.gwt.ui.client.subscription;
import com.google.gwt.core.client.JavaScriptObject;
/**
* Class represent JS option object for Chargebee portal instance open method
* {@link ChargebeePortal}
* Class represent JS option object for Chargebee portal instance open method {@link ChargebeePortal}
*
* @author tutran
*/
public class PortalOption extends JavaScriptObject {
protected PortalOption() {}
protected PortalOption() {
}
public static native PortalOption create(CloseCallback onClose) /*-{
return {
close: function() {
close : function() {
onClose.@com.sap.sailing.gwt.ui.client.subscription.PortalOption.CloseCallback::call()();
}
};
}-*/;
public static interface CloseCallback {
void call();
}
@@ -3,18 +3,18 @@ package com.sap.sailing.gwt.ui.client.subscription;
import com.google.gwt.core.client.JavaScriptObject;
/**
* Class represent Chargeebee portal session callback function
* {@link ChargebeeInstance}
* Class represent Chargeebee portal session callback function {@link ChargebeeInstance#setPortalSession(PortalSessionSetterCallback)}
* {@link https://www.chargebee.com/checkout-portal-docs/api.html#setportalsession}
*
* @author tutran
*/
public class PortalSessionSetterCallback extends JavaScriptObject {
protected PortalSessionSetterCallback() {}
protected PortalSessionSetterCallback() {
}
public static native PortalSessionSetterCallback create(String portalSession) /*-{
return function() {
return new Promise(function (resolve) {
return new Promise(function(resolve) {
resolve(JSON.parse(portalSession));
});
}
@@ -1,8 +1,7 @@
package com.sap.sailing.gwt.ui.client.subscription;
/**
* Chargebee configuration.
* From frontend side we need to know Chargebee site
* Chargebee configuration. From frontend side we need to know Chargebee site
*
* @author tutran
*/
@@ -10,8 +10,33 @@ import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
* @author tutran
*/
public interface SubscriptionService extends RemoteService {
/**
* Generate Chargebee checkout hosted page object. Client has to call this method to get the hosted page before
* opening Chargebee checkout modal Check flow of getting hosted page object from here
* {@link https://www.chargebee.com/checkout-portal-docs/api-checkout.html#call-flow}
*
* @param planId
* Plan id to subscribe to
*/
public HostedPageResultDTO generateHostedPageObject(String planId);
/**
* Call this method from frontend after checkout is success. This method will send acknowledge request to Chargebee
* for the checkout, and update user's subscription, and send back subscription information to frontend for updating
* view.
*
* @param hostedPageId
* the success hosted page id which is returned from Chargebee
*/
public SubscriptionDTO updatePlanSuccess(String hostedPageId);
/**
* Fetch user current subscription data from database
*/
public SubscriptionDTO getSubscription();
/**
* Cancel current user subscription
*/
public boolean cancelSubscription();
}
@@ -4,9 +4,17 @@ import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sailing.gwt.ui.shared.subscription.HostedPageResultDTO;
import com.sap.sailing.gwt.ui.shared.subscription.SubscriptionDTO;
/**
* Async remote service interface for {@link SubscriptionService}
*
* @author tutran
*/
public interface SubscriptionServiceAsync {
public void generateHostedPageObject(String planId, AsyncCallback<HostedPageResultDTO> callback);
public void updatePlanSuccess(String hostedPageId, AsyncCallback<SubscriptionDTO> callback);
public void getSubscription(AsyncCallback<SubscriptionDTO> callback);
void cancelSubscription(AsyncCallback<Boolean> callback);
}
@@ -4,43 +4,43 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Chargebee subscription configuration
* Chargebee subscription configuration We need the site and api key information. The system will get these information
* from server application startup arguments: chargebee.site, chargebee.apikey
*/
public class SubscriptionConfiguration {
private static final Logger logger = Logger.getLogger(SubscriptionConfiguration.class.getName());
private static final String CHARGEBEE_SITE = "chargebee.site";
private static final String CHARGEBEE_APIKEY = "chargebee.apikey";
private static SubscriptionConfiguration instance;
private String site;
private String apiKey;
protected SubscriptionConfiguration(String site, String apiKey) {
this.site = site;
this.apiKey = apiKey;
}
public static SubscriptionConfiguration getInstance() {
if (instance == null) {
String site = System.getProperty(CHARGEBEE_SITE);
String apiKey = System.getProperty(CHARGEBEE_APIKEY);
logger.log(Level.INFO, "Chargebee site: " + site + ", apiKey: " + apiKey);
instance = new SubscriptionConfiguration(site, apiKey);
}
return instance;
}
public String getSite() {
return site;
}
public String getApiKey() {
return apiKey;
}
}
@@ -26,21 +26,28 @@ import com.sap.sse.security.shared.Subscription;
import com.sap.sse.security.shared.impl.User;
import com.sap.sse.security.ui.server.Activator;
/**
* Backend implementation of {@link SubscriptionService} remote service interface.
*
* @author tutran
*/
public class SubscriptionServiceImpl extends RemoteServiceServlet implements SubscriptionService {
private static final long serialVersionUID = -4276839013785711262L;
private static final Logger logger = Logger.getLogger(SubscriptionServiceImpl.class.getName());
private final BundleContext context;
private final FutureTask<SecurityService> securityService;
public SubscriptionServiceImpl() {
Environment.configure(
SubscriptionConfiguration.getInstance().getSite(),
// Configure Chargebee
Environment.configure(SubscriptionConfiguration.getInstance().getSite(),
SubscriptionConfiguration.getInstance().getApiKey());
// get SecurityService
context = Activator.getContext();
final ServiceTracker<SecurityService, SecurityService> tracker = new ServiceTracker<>(context, SecurityService.class, /* customizer */ null);
final ServiceTracker<SecurityService, SecurityService> tracker = new ServiceTracker<>(context,
SecurityService.class, /* customizer */ null);
tracker.open();
securityService = new FutureTask<SecurityService>(new Callable<SecurityService>() {
@Override
@@ -49,7 +56,7 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
try {
logger.info("Waiting for SecurityService...");
result = tracker.waitForService(0);
logger.info("Obtained SecurityService "+result);
logger.info("Obtained SecurityService " + result);
return result;
} catch (InterruptedException e) {
logger.log(Level.SEVERE, "Interrupted while waiting for UserStore service", e);
@@ -57,7 +64,7 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
return result;
}
});
new Thread("ServiceTracker in bundle com.sap.sailing.gwt.ui.server for SecurityService") {
new Thread("ServiceTracker in bundle com.sap.sailing.gwt.ui.server.subscription for SecurityService") {
@Override
public void run() {
securityService.run();
@@ -68,22 +75,30 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
@Override
public HostedPageResultDTO generateHostedPageObject(String planId) {
HostedPageResultDTO response = new HostedPageResultDTO();
// Validate if plan id is valid
if (planId == null || planId.isEmpty() || SubscriptionPlans.getPlan(planId) == null) {
response.error = "Invalid plan";
return response;
}
try {
User user = getCurrentUser();
if (user.getSubscription() != null && user.getSubscription().planId != null && user.getSubscription().planId.equals(planId)) {
response.error = "User has already subscribed to " + SubscriptionPlans.getPlan(planId).getName() + " plan";
// 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)) {
response.error = "User has already subscribed to " + SubscriptionPlans.getPlan(planId).getName()
+ " plan";
return response;
}
Result result;
// If there's no subscription data attach to user model then we create a checkout-new request to Chargebee
if (user.getSubscription() == null || user.getSubscription().planId == null) {
String[] userNameParts = user.getFullName().split("\\s+");
String firstName = userNameParts[0];
@@ -91,45 +106,43 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
if (userNameParts.length > 1) {
lastName = String.join(" ", Arrays.copyOfRange(userNameParts, 1, userNameParts.length));
}
String locale = user.getLocaleOrDefault().getLanguage();
// Send checkout-new request to Chargebee with all necessary customer information and get back hosted
// page result object
result = HostedPage.checkoutNew()
.customerId(user.getName())
.customerEmail(user.getEmail())
.customerFirstName(firstName)
.customerLastName(lastName)
.customerLocale(locale)
.subscriptionPlanId(planId)
.billingAddressFirstName(firstName)
.billingAddressLastName(lastName)
.billingAddressCountry("US").request();
} else {
result = HostedPage.checkoutExisting()
.subscriptionId(user.getSubscription().subscriptionId)
.subscriptionPlanId(planId)
// here Chargebee's customer id is same as the system user name
.customerId(user.getName()).customerEmail(user.getEmail()).customerFirstName(firstName)
.customerLastName(lastName).customerLocale(locale).subscriptionPlanId(planId)
.billingAddressFirstName(firstName).billingAddressLastName(lastName).billingAddressCountry("US")
.request();
} else {
// User has already subscribed to a plan, so here user wants to change plan
// and we make a checkout-existing request to Chargebee, and get back hosted page object
result = HostedPage.checkoutExisting().subscriptionId(user.getSubscription().subscriptionId)
.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";
}
return response;
}
@Override
public SubscriptionDTO updatePlanSuccess(String hostedPageId) {
SubscriptionDTO subscriptionDto = new SubscriptionDTO();
try {
User user = getCurrentUser();
Result result = HostedPage.acknowledge(hostedPageId).request();
Content content = result.hostedPage().content();
Subscription subscription = new Subscription();
@@ -143,36 +156,35 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
subscription.subsciptionUpdatedAt = Math.round(content.subscription().updatedAt().getTime() / 1000);
subscription.latestEventTime = 0;
subscription.manualUpdatedAt = Math.round(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;
return subscriptionDto;
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in saving subscription ", e);
subscriptionDto.error = e.getMessage();
return subscriptionDto;
}
return subscriptionDto;
}
@Override
public SubscriptionDTO getSubscription() {
SubscriptionDTO subscriptionDto = new SubscriptionDTO();
try {
User user = getCurrentUser();
Subscription subscription = user.getSubscription();
if (subscription == null || subscription.planId == null || subscription.planId.isEmpty()) {
return null;
}
subscriptionDto.planId = subscription.planId;
subscriptionDto.subscriptionStatus = subscription.subscriptionStatus;
subscriptionDto.paymentStatus = subscription.paymentStatus;
@@ -181,13 +193,13 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
} catch (Exception e) {
e.printStackTrace();
logger.log(Level.WARNING, "Error in getting subscription ", e);
subscriptionDto.error = e.getMessage();
}
return subscriptionDto;
}
@Override
public boolean cancelSubscription() {
try {
@@ -196,16 +208,20 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
if (subscription == null) {
return true;
}
String subscriptionId = subscription.subscriptionId;
if (subscriptionId != null && !subscriptionId.isEmpty()) {
Result result = com.chargebee.models.Subscription.cancel(subscriptionId)
.request();
if (!result.subscription().status().name().toLowerCase().equals(Subscription.SUBSCRIPTION_STATUS_CANCELLED)) {
// Send cancel request to Chargebee and verify result and only process if the result's subscription
// status is updated to be cancelled
Result result = com.chargebee.models.Subscription.cancel(subscriptionId).request();
if (!result.subscription().status().name().toLowerCase()
.equals(Subscription.SUBSCRIPTION_STATUS_CANCELLED)) {
return false;
}
}
// for a cancelled subscription we'll 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);
@@ -218,20 +234,6 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
}
}
// @Override
// public String generatePortalPageObject() {
// try {
// User user = getCurrentUser();
// Result result = PortalSession.create().customerId(user.getName()).request();
// PortalSession portalSession = result.portalSession();
// return portalSession.toJson();
// } catch (Exception e) {
// e.printStackTrace();
// logger.log(Level.WARNING, "Error in generating portal session page object ", e);
// return null;
// }
// }
private SecurityService getSecurityService() {
try {
return securityService.get();
@@ -239,23 +241,23 @@ public class SubscriptionServiceImpl extends RemoteServiceServlet implements Sub
throw new RuntimeException(e);
}
}
private User getCurrentUser() throws SubscriptionException {
User user = getSecurityService().getCurrentUser();
if (user == null) {
throw new SubscriptionException(SubscriptionException.INVALID_CURRENT_USER);
}
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;
@@ -13,7 +13,7 @@ public class HostedPageResultDTO implements IsSerializable {
* In success case, hostedPageJSONString has value of JSON encoded string of hosted page object
*/
public String hostedPageJSONString;
/**
* In fail case, error contains error message
*/
@@ -2,32 +2,76 @@ package com.sap.sailing.gwt.ui.shared.subscription;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.sap.sailing.gwt.ui.client.StringMessages;
import com.sap.sailing.gwt.ui.client.subscription.SubscriptionService;
/**
* User subscription data transfer object {@link SubscriptionService}
*
* @author tutran
*/
public class SubscriptionDTO implements IsSerializable {
public static String PAYMENT_STATUS_SUCCESS = "success";
public static String PAYMENT_STATUS_NO_SUCCESS = "no_success";
public static String SUBSCRIPTION_STATUS_TRIAL = "in_trial";
public static String SUBSCRIPTION_STATUS_ACTIVE = "active";
/**
* User current subscription plan id
*/
public String planId;
/**
* Trial start timestamp
*/
public long trialStart;
/**
* Trial end timestamp
*/
public long trialEnd;
/**
* Subscription status: active or in_trial
*/
public String subscriptionStatus;
/**
* Subscription payment status: success or no_success
*/
public String paymentStatus;
/**
* Error message
*/
public String error;
/**
* Check if subscription is in trial status
*
* @return
*/
public boolean isInTrial() {
return subscriptionStatus.equals(SUBSCRIPTION_STATUS_TRIAL);
}
/**
* Check if subscription is in active status
*
* @return
*/
public boolean isActive() {
return subscriptionStatus.equals(SUBSCRIPTION_STATUS_ACTIVE);
}
public boolean isPaymentSuccess() {
return paymentStatus.equals(PAYMENT_STATUS_SUCCESS);
}
/**
* Get subscription status i18n label
*
* @return
*/
public String getSubscriptionStatusLabel() {
if (subscriptionStatus != null) {
if (subscriptionStatus.equals(SUBSCRIPTION_STATUS_TRIAL)) {
@@ -36,10 +80,15 @@ public class SubscriptionDTO implements IsSerializable {
return StringMessages.INSTANCE.active();
}
}
return "";
}
/**
* Get subscription payment status i18n label
*
* @return
*/
public String getPaymentStatusLabel() {
if (paymentStatus != null) {
if (paymentStatus.equals(PAYMENT_STATUS_SUCCESS)) {
@@ -48,7 +97,7 @@ public class SubscriptionDTO implements IsSerializable {
return StringMessages.INSTANCE.paymentStatusNoSuccess();
}
}
return "";
}
}
@@ -5,37 +5,42 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Class represents all subscription plans
*
* @author tutran
*/
public class SubscriptionPlans {
public static Plan STARTER = new Plan("starter", "Starter");
public static Plan PREMIUM = new Plan("premium", "Premium");
private static Map<String, Plan> planMap;
private static List<Plan> planList;
public static Plan getPlan(String planId) {
if (planMap == null) {
planMap = new HashMap<String, SubscriptionPlans.Plan>();
planMap.put(STARTER.id, STARTER);
planMap.put(PREMIUM.id, PREMIUM);
}
return planMap.get(planId);
}
public static List<Plan> getPlanList() {
if (planList == null) {
planList = new ArrayList<SubscriptionPlans.Plan>();
planList.add(STARTER);
planList.add(PREMIUM);
}
return planList;
}
public static class Plan {
private String name;
private String id;
public Plan(String id, String name) {
this.name = name;
this.id = id;
@@ -2,6 +2,11 @@ package com.sap.sailing.server.gateway.subscription;
import org.json.simple.JSONObject;
/**
* Wrapped class for Chargebee webhook event JSON object
*
* @author tutran
*/
public class SubscriptionWebhookEvent {
public static final String EVENT_CUSTOMER_DELETED = "customer_deleted";
public static final String EVENT_SUBSCRIPTION_DELETED = "subscription_deleted";
@@ -11,25 +16,25 @@ public class SubscriptionWebhookEvent {
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";
public static final String TRANSACTION_STATUS_SUCCESS = "success";
public static final String TRANSACTION_TYPE_PAYMENT = "payment";
private JSONObject eventJSON;
private String eventId;
private String eventType;
private JSONObject content;
public SubscriptionWebhookEvent(JSONObject eventJSON) {
this.eventJSON = eventJSON;
eventId = getJSONString(eventJSON, "id");
eventType = toLowerCase(getJSONString(eventJSON, "event_type"));
content = getJSONObject(eventJSON, "content");
}
public boolean isValidEvent() {
return eventId != null && eventType != null;
}
@@ -41,72 +46,72 @@ public class SubscriptionWebhookEvent {
public String getEventType() {
return eventType;
}
public String getCustomerEmail() {
return getNestedJSONString(content, "customer", "email");
}
public String getCustomerId() {
return getNestedJSONString(content, "customer", "id");
}
public String getPlanId() {
return getNestedJSONString(content, "subscription", "plan_id");
}
public String getSubscriptionId() {
return getNestedJSONString(content, "subscription", "id");
}
public String getSubscriptionStatus() {
return toLowerCase(getNestedJSONString(content, "subscription", "status"));
}
public long getSubscriptionTrialStart() {
return getNestedJSONLong(content, "subscription", "trial_start");
}
public long getSubscriptionTrialEnd() {
return getNestedJSONLong(content, "subscription", "trial_end");
}
public long getSubscriptionCreatedAt() {
return getNestedJSONLong(content, "subscription", "created_at");
}
public long getSubscriptionUpdatedAt() {
return getNestedJSONLong(content, "subscription", "updated_at");
}
public long getEventOccurredAt() {
return getJSONLong(eventJSON, "occurred_at");
}
public String getTransactionStatus() {
return toLowerCase(getNestedJSONString(content, "transaction", "status"));
}
public String getInvoiceStatus() {
return toLowerCase(getNestedJSONString(content, "invoice", "status"));
}
public String getTransactionType() {
return toLowerCase(getNestedJSONString(content, "transaction", "type"));
}
private JSONObject getJSONObject(JSONObject object, String key) {
return (JSONObject)object.get(key);
return (JSONObject) object.get(key);
}
private String getJSONString(JSONObject object, String key) {
return (String)object.get(key);
return (String) object.get(key);
}
private long getJSONLong(JSONObject object, String key) {
return (Long)object.get(key);
return (Long) object.get(key);
}
private Object getNestedJSONValue(JSONObject object, String ...keys) {
private Object getNestedJSONValue(JSONObject object, String... keys) {
JSONObject tmp = object;
Object val = null;
for (int i = 0; i < keys.length; i++) {
@@ -119,18 +124,18 @@ public class SubscriptionWebhookEvent {
val = tmp.get(keys[i]);
}
}
return val;
}
private String getNestedJSONString(JSONObject object, String ...keys) {
return (String)getNestedJSONValue(object, keys);
private String getNestedJSONString(JSONObject object, String... keys) {
return (String) getNestedJSONValue(object, keys);
}
private long getNestedJSONLong(JSONObject object, String ...keys) {
return (Long)getNestedJSONValue(object, keys);
private long getNestedJSONLong(JSONObject object, String... keys) {
return (Long) getNestedJSONValue(object, keys);
}
private String toLowerCase(String str) {
return str != null ? str.toLowerCase() : null;
}
@@ -19,54 +19,64 @@ import com.sap.sse.security.shared.Subscription;
import com.sap.sse.security.shared.UserManagementException;
import com.sap.sse.security.shared.impl.User;
/**
* Servlet for handling Chargebee webhook events Response with status 200 in success handling
*
* {@link https://www.chargebee.com/docs/events_and_webhooks.html}
*
* @author tutran
*/
public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
private static final long serialVersionUID = 2608645647937414012L;
private static final Logger logger = Logger.getLogger(SubscriptionWebhookServlet.class.getName());
private static String basicAuthHeaderValue;
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (!verifyBasicAuth(request)) {
logger.log(Level.WARNING, "Invalid webhook http basic auth");
response.setStatus(403);
return;
}
try {
Object requestBody = JSONValue.parseWithException(request.getReader());
JSONObject requestObject = Helpers.toJSONObjectSafe(requestBody);
logger.log(Level.INFO, "Chargebee webhook data: " + requestObject.toJSONString());
SubscriptionWebhookEvent event = new SubscriptionWebhookEvent(requestObject);
if (!event.isValidEvent()) {
throw new Exception("Invalid webhook event");
}
User user = getUser(event.getCustomerId());
if (user == null) {
response.setStatus(200);
return;
}
Subscription userSubscription = user.getSubscription();
long occuredAt = event.getEventOccurredAt();
if (userSubscription != null &&
occuredAt < user.getSubscription().latestEventTime &&
occuredAt < user.getSubscription().manualUpdatedAt) {
// 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);
return;
}
switch (event.getEventType()) {
case SubscriptionWebhookEvent.EVENT_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())) {
if (userSubscription != null && userSubscription.subscriptionId != null
&& userSubscription.subscriptionId.equals(event.getSubscriptionId())) {
updateUserSubscription(user, buildEmptySubscription(userSubscription, event));
}
break;
@@ -78,7 +88,7 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
updateUserSubscription(user, buildSubscription(userSubscription, event));
break;
}
response.setStatus(200);
} catch (ParseException e) {
e.printStackTrace();
@@ -90,17 +100,17 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
response.setStatus(500);
}
}
private Subscription buildEmptySubscription(Subscription currentSubscription, SubscriptionWebhookEvent event) {
Subscription subscription = new Subscription();
subscription.latestEventTime = event.getEventOccurredAt();
subscription.manualUpdatedAt = currentSubscription != null ? currentSubscription.manualUpdatedAt : 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();
@@ -111,8 +121,9 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
subscription.subsciptionUpdatedAt = event.getSubscriptionUpdatedAt();
subscription.latestEventTime = event.getEventOccurredAt();
subscription.manualUpdatedAt = currentSubscription != null ? currentSubscription.manualUpdatedAt : 0;
if (subscription.subscriptionStatus != null && subscription.subscriptionStatus.equals(SubscriptionWebhookEvent.SUBSCRIPTION_STATUS_ACTIVE)) {
if (subscription.subscriptionStatus != null
&& subscription.subscriptionStatus.equals(SubscriptionWebhookEvent.SUBSCRIPTION_STATUS_ACTIVE)) {
String paymentStatus = getEventPaymentStatus(event);
if (paymentStatus != null) {
subscription.paymentStatus = paymentStatus;
@@ -122,52 +133,53 @@ public class SubscriptionWebhookServlet extends SailingServerHttpServlet {
} else {
subscription.paymentStatus = null;
}
return subscription;
}
private User getUser(String customerId) {
return getSecurityService().getUserByName(customerId);
}
private void updateUserSubscription(User user, Subscription subscription) throws UserManagementException {
getSecurityService().updateUserSubscription(user.getName(), subscription);
}
private String getEventPaymentStatus(SubscriptionWebhookEvent event) {
String paymentStatus = null;
String transactionStatus = event.getTransactionStatus();
if (transactionStatus == null) {
String invoiceStatus = event.getInvoiceStatus();
if (invoiceStatus != null) {
paymentStatus = invoiceStatus.equals(SubscriptionWebhookEvent.INVOICE_STATUS_PAID) ?
Subscription.PAYMENT_STATUS_SUCCESS :
Subscription.PAYMENT_STATUS_NO_SUCCESS;
paymentStatus = invoiceStatus.equals(SubscriptionWebhookEvent.INVOICE_STATUS_PAID)
? Subscription.PAYMENT_STATUS_SUCCESS
: Subscription.PAYMENT_STATUS_NO_SUCCESS;
}
} else {
String transactionType = event.getTransactionType();
if (transactionType != null && transactionType.equals(SubscriptionWebhookEvent.TRANSACTION_TYPE_PAYMENT)) {
paymentStatus = transactionStatus.equals(SubscriptionWebhookEvent.TRANSACTION_STATUS_SUCCESS) ?
Subscription.PAYMENT_STATUS_SUCCESS :
Subscription.PAYMENT_STATUS_NO_SUCCESS;
paymentStatus = transactionStatus.equals(SubscriptionWebhookEvent.TRANSACTION_STATUS_SUCCESS)
? Subscription.PAYMENT_STATUS_SUCCESS
: Subscription.PAYMENT_STATUS_NO_SUCCESS;
}
}
return paymentStatus;
}
private boolean verifyBasicAuth(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null) {
return false;
}
if (basicAuthHeaderValue == null) {
String cred = WebhookBasicAuthConfiguration.getInstance().getUsername() + ":" + WebhookBasicAuthConfiguration.getInstance().getPassword();
String cred = WebhookBasicAuthConfiguration.getInstance().getUsername() + ":"
+ WebhookBasicAuthConfiguration.getInstance().getPassword();
String base64Hash = Base64.getEncoder().encodeToString(cred.getBytes());
basicAuthHeaderValue = "Basic " + base64Hash;
}
return authHeader.equals(basicAuthHeaderValue);
}
}
@@ -1,25 +1,28 @@
package com.sap.sailing.server.gateway.subscription;
/**
* Chargebee webhook basic authentication configuration. The basic auth user and password have to be in server
* application start arguments: chargebee.basicauthuser, chargebee.basicauthpass
*
* @author tutran
*/
public class WebhookBasicAuthConfiguration {
private static final String USER = "chargebee.basicauthuser";
private static final String PASSWORD = "chargebee.basicauthpass";
private static WebhookBasicAuthConfiguration instance;
private String username;
private String password;
public static WebhookBasicAuthConfiguration getInstance() {
if (instance == null) {
instance = new WebhookBasicAuthConfiguration(
System.getProperty(USER),
System.getProperty(PASSWORD)
);
instance = new WebhookBasicAuthConfiguration(System.getProperty(USER), System.getProperty(PASSWORD));
}
return instance;
}
public WebhookBasicAuthConfiguration(String username, String password) {
this.username = username;
this.password = password;
@@ -2,6 +2,11 @@ package com.sap.sse.security.shared;
import java.io.Serializable;
/**
* Subscription data model for user which is persisted into database as subscription property of a user
*
* @author tutran
*/
public class Subscription implements Serializable {
public static String PAYMENT_STATUS_SUCCESS = "success";
public static String PAYMENT_STATUS_NO_SUCCESS = "no_success";
@@ -11,15 +16,69 @@ public class Subscription implements Serializable {
private static final long serialVersionUID = 96845123954667808L;
/**
* Subscription id from Chargebee
*/
public String subscriptionId;
/**
* Current subscription plan id
*/
public String planId;
/**
* Chargebee's customer id, this is same as the system's user name
*/
public String customerId;
/**
* Subscription trial start timestamp
*/
public long trialStart;
/**
* Subscription trial end timestamp
*/
public long trialEnd;
/**
* Subscription status, it could be in_trial, active, or cancelled.
* in_tiral means the subscription is in trial period
* active means the subscription is active
* cancelled means the subscription has been cancelled(by user or by admin from Chargebee dashboard)
*/
public 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;
/**
* Record the creating timestamp of the Chargebee's subscription
*/
public long subsciptionCreatedAt;
/**
* Record the updating timestamp of the Chargebee's subscription
*/
public long subsciptionUpdatedAt;
/**
* Record the timestamp of the latest handled webhook event.
* Because a webhook event might be retried to send to our system by Chargebee later if it has failed on the prev times,
* so we will this timestamp to only process a newer event, otherwise we will update wrong data for a user.
*
* {@link https://www.chargebee.com/docs/webhook_settings.html#automatic-retries}
*/
public 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;
}
@@ -683,6 +683,13 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
void registerCustomizer(SecurityInitializationCustomizer customizer);
/**
* Persist user subscription data
*
* @param username
* @param subscription
* @throws UserManagementException
*/
void updateUserSubscription(String username, Subscription subscription) throws UserManagementException;
}
@@ -3,6 +3,11 @@ package com.sap.sse.security.operations;
import com.sap.sse.security.impl.ReplicableSecurityService;
import com.sap.sse.security.shared.Subscription;
/**
* Update user's subscription operation
*
* @author tutran
*/
public class UpdateUserSubscriptionOperation implements SecurityOperation<Void> {
private static final long serialVersionUID = 4943500215851172841L;