Skip to content

Commit

Permalink
Merge pull request #324 from Adyen/feature/AD-20
Browse files Browse the repository at this point in the history
[AD-20] Recurring payment for subscription
  • Loading branch information
kpieloch authored Nov 7, 2023
2 parents cf077a0 + 42bbd1d commit bc9db51
Show file tree
Hide file tree
Showing 22 changed files with 1,038 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ public class DefaultAdyenCheckoutFacade implements AdyenCheckoutFacade {
private static final String IT_LOCALE = "it_IT";
private static final String ES_LOCALE = "es_ES";
private static final String US = "US";
private static final String RECURRING_RECURRING_DETAIL_REFERENCE = "recurring.recurringDetailReference";

private BaseStoreService baseStoreService;
private SessionService sessionService;
Expand Down Expand Up @@ -534,12 +535,24 @@ private OrderData createAuthorizedOrder(final PaymentsResponse paymentsResponse)
final CartModel cartModel = cartService.getSessionCart();
final String merchantTransactionCode = cartModel.getCode();

//First save the transactions to the CartModel < AbstractOrderModel
updateAdyenSelectedReferenceIfPresent(cartModel, paymentsResponse);

// First save the transactions to the CartModel < AbstractOrderModel
getAdyenTransactionService().authorizeOrderModel(cartModel, merchantTransactionCode, paymentsResponse.getPspReference());

return createOrderFromPaymentsResponse(paymentsResponse);
}

private void updateAdyenSelectedReferenceIfPresent(final CartModel cartModel, final PaymentsResponse paymentsResponse) {
Map<String, String> additionalData = paymentsResponse.getAdditionalData();
if (additionalData != null) {
String recurringDetailReference = additionalData.get(RECURRING_RECURRING_DETAIL_REFERENCE);
if (recurringDetailReference != null) {
cartModel.getPaymentInfo().setAdyenSelectedReference(recurringDetailReference);
}
}
}

/**
* Create order and authorized TX
*/
Expand Down
2 changes: 1 addition & 1 deletion adyenv6core/src/com/adyen/v6/model/RequestInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public RequestInfo(HttpServletRequest request) {
this.origin = getOrigin(request);
}

private RequestInfo() {
public RequestInfo() {
}

public String getOrigin(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public void testAuthorise() {
testRecurringOption(RecurringContractMode.NONE, null);
testRecurringOption(RecurringContractMode.ONECLICK, null);
//testRecurringOption(RecurringContractMode.RECURRING, Recurring.ContractEnum.RECURRING);
//testRecurringOption(RecurringContractMode.ONECLICK_RECURRING, Recurring.ContractEnum.RECURRING);
//testRecurringOption(RecurringContractMode.RECURRING, Recurring.ContractEnum.RECURRING);

//Test recurring contract when remember-me is set
when(cartDataMock.getAdyenRememberTheseDetails()).thenReturn(true);
Expand Down
11 changes: 2 additions & 9 deletions adyenv6subscription/extensioninfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,10 @@
<requires-extension name="commercefacades"/>
<requires-extension name="basecommerce"/>
<requires-extension name="adyenv6core"/>

<requires-extension name="subscriptionservices"/>


<coremodule generated="true" manager="com.adyen.v6.jalo.Adyenv6subscriptionManager" packageroot="com.adyen.v6"/>


<webmodule jspcompile="false" webroot="/adyenv6subscription"/>





</extension>


Expand Down
3 changes: 3 additions & 0 deletions adyenv6subscription/resources/adyenv6subscription-beans.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@

<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="beans.xsd">

<bean class="de.hybris.platform.commercefacades.order.data.AbstractOrderData">
<property name="subscriptionOrder" type="java.lang.Boolean" />
</bean>

</beans>
25 changes: 25 additions & 0 deletions adyenv6subscription/resources/adyenv6subscription-items.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@
</attributes>
</itemtype>

<itemtype code="AbstractOrder" autocreate="false" generate="false">
<attributes>
<attribute type="java.lang.Boolean" qualifier="subscriptionOrder">
<description>If the order is generated by subscription job</description>
<persistence type="property" />
<modifiers optional="true" unique="false" write="true" read="true" />
</attribute>
</attributes>
</itemtype>

<itemtype code="BaseStore" generate="false" autocreate="false">
<attributes>
<attribute qualifier="subscriptionAllowedPaymentMethods" type="SubscriptionAllowedPaymentMethods" >
Expand All @@ -35,5 +45,20 @@
</attributes>
</itemtype>

<itemtype code="Subscription" autocreate="false" generate="false">
<attributes>
<attribute type="Order" qualifier="subscriptionOrder">
<description>The order the subcription is created from</description>
<persistence type="property" />
<modifiers optional="false" unique="false" write="true" read="true" />
</attribute>
<attribute qualifier="nextChargeDate" type="java.util.Date">
<description>The next date that customer should be charged</description>
<modifiers read="true" write="true" search="true" optional="true" unique="false"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>

</itemtypes>
</items>
87 changes: 83 additions & 4 deletions adyenv6subscription/resources/adyenv6subscription-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
Copyright (c) 2021 SAP SE or an SAP affiliate company. All rights reserved.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<alias name="subscriptionAdyenPaymentServiceFactory" alias="adyenPaymentServiceFactory" />
<bean id="subscriptionAdyenPaymentServiceFactory" class="com.adyen.v6.factory.SubscriptionAdyenPaymentServiceFactory">
Expand All @@ -22,4 +22,83 @@
<property name="cartFacade" ref="cartFacade" />
</bean>

<bean id="subscriptionRepository" class="com.adyen.v6.repository.SubscriptionRepository" >
<property name="flexibleSearchService" ref="flexibleSearchService" />
</bean>

<bean id="subscriptionOrderExecutor" class="com.adyen.v6.service.impl.SubscriptionOrderExecutor" scope="prototype">
<constructor-arg value="originalOrder" />
<property name="cartService" ref="defaultCartService" />
<property name="typeService" ref="typeService" />
<property name="adyenPaymentServiceFactory" ref="adyenPaymentServiceFactory" />
<property name="adyenTransactionService" ref="adyenTransactionService" />
<property name="cartConverter" ref="cartConverter" />
<property name="commerceCheckoutService" ref="commerceCheckoutService" />
<property name="baseStoreService" ref="baseStoreService" />
<property name="modelService" ref="modelService" />
<property name="keyGenerator" ref="orderCodeGenerator" />
<property name="subscriptionAdyenPaymentServiceFactory" ref="subscriptionAdyenPaymentServiceFactory" />
</bean>

<alias alias="subscriptionCommercePlaceOrderMethodHook" name="adyenSubscriptionCommercePlaceOrderMethodHook"/>
<bean id="adyenSubscriptionCommercePlaceOrderMethodHook"
class="com.adyen.v6.hooks.AdyenSubscriptionCommercePlaceOrderMethodHook"
parent="defaultSubscriptionCommercePlaceOrderMethodHook">

</bean>

<bean id="adyenSubscriptionOrderPopulator" class="com.adyen.v6.populators.AdyenSubscriptionOrderPopulator" parent="baseOrderPopulator">
</bean>

<bean parent="modifyPopulatorList">
<property name="list" ref="cartConverter"/>
<property name="add" ref="adyenSubscriptionOrderPopulator"/>
</bean>

<bean parent="modifyPopulatorList">
<property name="list" ref="extendedCartConverter"/>
<property name="add" ref="adyenSubscriptionOrderPopulator"/>
</bean>

<bean parent="modifyPopulatorList">
<property name="list" ref="orderConverter"/>
<property name="add" ref="adyenSubscriptionOrderPopulator"/>
</bean>

<bean id="recurringOrderService" class="com.adyen.v6.service.impl.DefaultRecurringOrderService">
<property name="impersonationService" ref="impersonationService"/>
</bean>

<bean id="subscriptionCronJob" class="com.adyen.v6.job.SubscriptionCronJob" parent="abstractJobPerformable" >
<property name="subscriptionRepository" ref="subscriptionRepository" />
<property name="recurringOrderService" ref="recurringOrderService" />
</bean>

<alias alias="recurringOrdersCloneAbstractOrderHook" name="adyenRecurringOrdersCloneAbstractOrderHook"/>
<bean id="adyenRecurringOrdersCloneAbstractOrderHook" class="com.adyen.v6.hooks.AdyenRecurringOrdersCloneAbstractOrderHook">
<property name="itemModelCloneCreator" ref="itemModelCloneCreator"/>
<property name="modelService" ref="modelService"/>
</bean>

<util:list id="recurringOrdersCloneAbstractOrderHookList"
value-type="de.hybris.platform.order.strategies.ordercloning.CloneAbstractOrderHook" >
<ref bean="recurringOrdersCloneAbstractOrderHook"/>
</util:list>

<bean id="recurringOrdersCloneAbstractOrderStrategy"
class="de.hybris.platform.order.strategies.ordercloning.impl.DefaultCloneAbstractOrderStrategy">
<constructor-arg name="typeService" ref="typeService"/>
<constructor-arg name="itemModelCloneCreator" ref="itemModelCloneCreator"/>
<constructor-arg name="abstractOrderEntryTypeService" ref="abstractOrderEntryTypeService"/>
<constructor-arg name="skippedAttributes" ref="recurringOrdersCloneAbstractOrderSkippedAttributesList" />
<property name="cloneHooks" ref="recurringOrdersCloneAbstractOrderHookList" />
</bean>

<util:list id="recurringOrdersCloneAbstractOrderSkippedAttributesList" value-type="java.lang.String">
<value>paymentInfo</value>
<value>deliveryAddress</value>
<value>paymentAddress</value>
<value>parent</value>
</util:list>

</beans>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
INSERT_UPDATE CronJob; code[unique=true];job(code);singleExecutable;sessionLanguage(isocode)
;subscriptionCronJob;subscriptionCronJob;false;en

INSERT_UPDATE Trigger;cronjob(code)[unique=true];cronExpression
;subscriptionCronJob; 0 0 2 * * ?
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,145 @@

import com.adyen.model.checkout.PaymentMethodDetails;
import com.adyen.model.checkout.PaymentsRequest;
import com.adyen.model.checkout.details.CardDetails;
import com.adyen.model.recurring.RecurringDetailsRequest;
import com.adyen.util.Util;
import com.adyen.v6.constants.Adyenv6coreConstants;
import com.adyen.v6.enums.RecurringContractMode;
import com.adyen.v6.model.RequestInfo;
import com.adyen.v6.paymentmethoddetails.executors.AdyenPaymentMethodDetailsBuilderExecutor;
import com.adyen.v6.utils.SubscriptionsUtils;
import de.hybris.platform.commercefacades.order.CartFacade;
import de.hybris.platform.commercefacades.order.data.CartData;
import de.hybris.platform.commercefacades.user.data.AddressData;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.servicelayer.config.ConfigurationService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMethod;


public class SubscriptionPaymentRequestFactory extends AdyenRequestFactory {

private static final Logger LOG = LoggerFactory.getLogger(SubscriptionPaymentRequestFactory.class);

@Resource(name = "cartFacade")
private CartFacade cartFacade;

public SubscriptionPaymentRequestFactory(ConfigurationService configurationService, AdyenPaymentMethodDetailsBuilderExecutor adyenPaymentMethodDetailsBuilderExecutor) {
super(configurationService, adyenPaymentMethodDetailsBuilderExecutor);
}

@Override
public PaymentsRequest createPaymentsRequest(String merchantAccount, CartData cartData, RequestInfo requestInfo, CustomerModel customerModel, RecurringContractMode recurringContractMode, Boolean guestUserTokenizationEnabled) {
PaymentsRequest paymentsRequest = super.createPaymentsRequest(merchantAccount, cartData, requestInfo, customerModel, recurringContractMode, guestUserTokenizationEnabled);
paymentsRequest.setShopperInteraction(PaymentsRequest.ShopperInteractionEnum.ECOMMERCE);
if (BooleanUtils.isTrue(SubscriptionsUtils.containsSubscription(getCartFacade().getSessionCart()))) {
paymentsRequest.setRecurringProcessingModel(PaymentsRequest.RecurringProcessingModelEnum.SUBSCRIPTION);
} else {
paymentsRequest.setRecurringProcessingModel(PaymentsRequest.RecurringProcessingModelEnum.CARDONFILE);
public PaymentsRequest createPaymentsRequest(String merchantAccount, CartData cartData, RequestInfo requestInfo,
CustomerModel customerModel, RecurringContractMode recurringContractMode,
Boolean guestUserTokenizationEnabled) {

LOG.info("Creating PaymentsRequest for merchant account: {}", merchantAccount);

if (BooleanUtils.isTrue(cartData.getSubscriptionOrder())) {
return createRecurringPaymentsRequest(merchantAccount, cartData, requestInfo, customerModel,
recurringContractMode, guestUserTokenizationEnabled);
}

return createRegularPaymentsRequest(merchantAccount, cartData, requestInfo, customerModel,
recurringContractMode, guestUserTokenizationEnabled);
}

private PaymentsRequest createRegularPaymentsRequest(String merchantAccount, CartData cartData, RequestInfo requestInfo,
CustomerModel customerModel, RecurringContractMode recurringContractMode,
Boolean guestUserTokenizationEnabled) {
LOG.info("Creating regular PaymentsRequest...");
PaymentsRequest paymentsRequest = super.createPaymentsRequest(merchantAccount, cartData, requestInfo,
customerModel, recurringContractMode, guestUserTokenizationEnabled);
paymentsRequest.setShopperInteraction(PaymentsRequest.ShopperInteractionEnum.CONTAUTH);

PaymentsRequest.RecurringProcessingModelEnum recurringProcessingModel = BooleanUtils.isTrue(
SubscriptionsUtils.containsSubscription(getCartFacade().getSessionCart()))
? PaymentsRequest.RecurringProcessingModelEnum.SUBSCRIPTION
: PaymentsRequest.RecurringProcessingModelEnum.CARDONFILE;

paymentsRequest.setRecurringProcessingModel(recurringProcessingModel);
return paymentsRequest;
}

protected PaymentsRequest createRecurringPaymentsRequest(final String merchantAccount, final CartData cartData,
final RequestInfo requestInfo, final CustomerModel customerModel, final RecurringContractMode recurringContractMode,
final Boolean guestUserTokenizationEnabled)
{

LOG.info("Creating RecurringPaymentsRequest for merchant account: {}", merchantAccount);

final PaymentsRequest paymentsRequest = new PaymentsRequest();
final String adyenPaymentMethod = cartData.getAdyenPaymentMethod();

if (adyenPaymentMethod == null)
{
throw new IllegalArgumentException("Payment method is null");
}

updatePaymentRequest(merchantAccount, cartData, requestInfo, customerModel, paymentsRequest);

final PaymentMethodDetails paymentMethod = adyenPaymentMethodDetailsBuilderExecutor.createPaymentMethodDetails(cartData);
if(paymentMethod instanceof CardDetails) {
((CardDetails) paymentMethod).setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
}
paymentMethod.setType(Adyenv6coreConstants.PAYMENT_METHOD_SCHEME);
paymentsRequest.setPaymentMethod(paymentMethod);


updateApplicationInfoEcom(paymentsRequest.getApplicationInfo());


paymentsRequest.setRedirectFromIssuerMethod(RequestMethod.POST.toString());
paymentsRequest.setRedirectToIssuerMethod(RequestMethod.POST.toString());
paymentsRequest.setShopperInteraction(PaymentsRequest.ShopperInteractionEnum.CONTAUTH);
paymentsRequest.setRecurringProcessingModel(PaymentsRequest.RecurringProcessingModelEnum.SUBSCRIPTION);

return paymentsRequest;
}


private void updatePaymentRequest(final String merchantAccount, final CartData cartData, final RequestInfo requestInfo,
final CustomerModel customerModel, final PaymentsRequest paymentsRequest)
{


final String currency = cartData.getTotalPrice().getCurrencyIso();
final String reference = cartData.getCode();

final AddressData billingAddress = cartData.getPaymentInfo() != null ? cartData.getPaymentInfo().getBillingAddress() : null;
final AddressData deliveryAddress = cartData.getDeliveryAddress();

paymentsRequest.amount(Util.createAmount(cartData.getTotalPrice().getValue(), currency)).reference(reference).merchantAccount(merchantAccount)
.setCountryCode(getCountryCode(cartData));
// set shopper details from CustomerModel.
if (customerModel != null)
{
paymentsRequest.setShopperReference(customerModel.getCustomerID());
paymentsRequest.setShopperEmail(customerModel.getContactEmail());
}

// if address details are provided, set it to the PaymentRequest
if (deliveryAddress != null)
{
paymentsRequest.setDeliveryAddress(setAddressData(deliveryAddress));
}

if (billingAddress != null)
{
paymentsRequest.setBillingAddress(setAddressData(billingAddress));
// set PhoneNumber if it is provided
final String phone = billingAddress.getPhone();
if (StringUtils.isNotBlank(phone ))
{
paymentsRequest.setTelephoneNumber(phone);
}
}

}

@Override
public RecurringDetailsRequest createListRecurringDetailsRequest(String merchantAccount, String customerId) {
if (SubscriptionsUtils.containsSubscription(getCartFacade().getSessionCart())) {
Expand All @@ -52,6 +151,7 @@ public RecurringDetailsRequest createListRecurringDetailsRequest(String merchant
}



protected CartFacade getCartFacade() {
return cartFacade;
}
Expand Down
Loading

0 comments on commit bc9db51

Please sign in to comment.