Skip to content
13 changes: 13 additions & 0 deletions adyenv6core/resources/adyenv6core-beans.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<property name="adyenMultibancoDeadline" type="java.lang.String" />
<property name="adyenMultibancoReference" type="java.lang.String" />
<property name="adyenPosReceipt" type="java.lang.String" />
<property name="subscriptionOrder" type="java.lang.Boolean" />
</bean>

<bean class="de.hybris.platform.commercefacades.order.data.OrderEntryData">
Expand Down Expand Up @@ -134,6 +135,18 @@
<property name="dataCollectionEnabled" type="boolean"/>
</bean>

<bean class="com.adyen.commerce.data.TokenWebhookRequestData">
<property name="createdAt" type="String"/>
<property name="eventId" type="String"/>
<property name="environment" type="String"/>
<property name="eventType" type="String"/>
<property name="merchantAccount" type="String"/>
<property name="operation" type="String"/>
<property name="shopperReference" type="String"/>
<property name="storedPaymentMethodId" type="String"/>
<property name="paymentType" type="String"/>
</bean>

<bean class="com.adyen.commerce.data.AdyenPartialPaymentOrderData">
<property name="pspReference" type="String"/>
<property name="requestAmount" type="java.math.BigDecimal"/>
Expand Down
5 changes: 5 additions & 0 deletions adyenv6core/resources/adyenv6core-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,11 @@
<property name="adyenNotificationService" ref="adyenNotificationService"/>
</bean>

<bean id="tokenizationWebhookEventListener" class="com.adyen.v6.listeners.TokenizationWebhookEventListener" parent="abstractEventListener">
<property name="paymentTransactionRepository" ref="adyenPaymentTransactionRepository"/>
<property name="modelService" ref="modelService"/>
</bean>

<bean id="adyenExpressCheckoutFacade" class="com.adyen.v6.facades.impl.DefaultAdyenExpressCheckoutFacade">
<property name="cartFactory" ref="cartFactory"/>
<property name="cartService" ref="cartService"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.adyen.commerce.services.impl;

import com.adyen.model.checkout.CardDetails;
import com.adyen.model.checkout.CheckoutPaymentMethod;
import com.adyen.model.checkout.PaymentRequest;
import com.adyen.v6.enums.RecurringContractMode;
import de.hybris.platform.commercefacades.order.data.CartData;
import de.hybris.platform.core.model.user.CustomerModel;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Objects;

public class CreditCardSubscriptionHandler implements PaymentMethodHandler {
@Override
public boolean canHandle(String paymentMethod) {
return Arrays.stream(CardDetails.TypeEnum.values()).map(CardDetails.TypeEnum::toString).anyMatch(type -> type.equalsIgnoreCase(paymentMethod));
}

@Override
public void updatePaymentRequest(PaymentRequest paymentRequest, CartData cartData, RecurringContractMode recurringContractMode, CustomerModel customerModel, Boolean is3DS2Allowed, Boolean guestUserTokenizationEnabled) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

CardSubscriptionHandler.java, IdealSubscriptionHandler.java, KlarnaSubscriptionHandler.java, PayPalSubscriptionHandler.java

These four files share almost identical logic in updatePaymentRequest. Did you consider a generalisation of this code ? For example:

PaymentMethodHandler {

    protected abstract T getPaymentMethodDetails(PaymentRequest request);
    protected abstract T createNewPaymentMethodDetails();

    @Override
    public void updatePaymentRequest(PaymentRequest paymentRequest, CartData cartData, ...) {
        if (StringUtils.isNotEmpty(cartData.getAdyenSelectedReference())) {
            T details = getPaymentMethodDetails(paymentRequest);
            if (details == null) {
                details = createNewPaymentMethodDetails();
            }
            
            details.setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
            
            CheckoutPaymentMethod checkoutPaymentMethod = new CheckoutPaymentMethod();
            checkoutPaymentMethod.setActualInstance(details);
            paymentRequest.setPaymentMethod(checkoutPaymentMethod);

            paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);
            paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.CONTAUTH);
        }
    }
}

if (StringUtils.isNotEmpty(cartData.getAdyenSelectedReference())) {

CardDetails cardDetails;

if (Objects.isNull(paymentRequest.getPaymentMethod()) || Objects.isNull(paymentRequest.getPaymentMethod().getCardDetails())) {
cardDetails = new CardDetails();
} else {
cardDetails = paymentRequest.getPaymentMethod().getCardDetails();
}

cardDetails.setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
CheckoutPaymentMethod checkoutPaymentMethod = new CheckoutPaymentMethod();
checkoutPaymentMethod.setActualInstance(cardDetails);
paymentRequest.setPaymentMethod(checkoutPaymentMethod);

paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);

paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.CONTAUTH);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public PaymentRequest createPaymentsRequest(final String merchantAccount,
handlePaymentMethodSpecificLogic(paymentRequest, cartData, originPaymentsRequest,
recurringContractMode, customerModel, guestUserTokenizationEnabled);

addPaymentMethodTokenizationToPaymentRequest(paymentRequest, cartData);

return paymentRequest;
}

Expand Down Expand Up @@ -396,7 +398,7 @@ protected void handlePaymentMethodSpecificLogic(PaymentRequest paymentRequest, C

// Use payment method handler
paymentMethodHandlerFactory.getHandler(paymentMethod)
.ifPresent(handler -> handler.updatePaymentRequest(paymentRequest, cartData,
.forEach(handler -> handler.updatePaymentRequest(paymentRequest, cartData,
recurringContractMode, customerModel, is3DS2Allowed, guestUserTokenizationEnabled));
}

Expand All @@ -406,6 +408,18 @@ protected void copySchemePaymentSettings(PaymentRequest paymentRequest, PaymentR
paymentRequest.setStorePaymentMethod(originPaymentsRequest.getStorePaymentMethod());
}

protected void addPaymentMethodTokenizationToPaymentRequest(PaymentRequest paymentRequest, CartData cartData) {
if (tokenizeForSubscriptionProducts(cartData)) {
paymentRequest.setStorePaymentMethod(true);
paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);
}
}

protected boolean tokenizeForSubscriptionProducts(CartData cartData) {
return !cartData.getSubscriptionOrder() && cartData.getEntries().stream()
.anyMatch(entry -> Objects.nonNull(entry.getProduct().getSubscriptionTerm()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

This logic assumes that cartData.getSubscriptionOrder() is false for the initial checkout and true only for the subsequent recurring runs. This might throw a NullPointerException (unless it's a primitive boolean, but the bean definition showed java.lang.Boolean).

}

protected AddressData getBillingAddress(CartData cartData) {
return Optional.ofNullable(cartData.getPaymentInfo())
.map(paymentInfo -> paymentInfo.getBillingAddress())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.adyen.commerce.services.impl;

import com.adyen.model.checkout.CheckoutPaymentMethod;
import com.adyen.model.checkout.IdealDetails;
import com.adyen.model.checkout.PaymentRequest;
import com.adyen.model.checkout.SepaDirectDebitDetails;
import com.adyen.v6.enums.RecurringContractMode;
import de.hybris.platform.commercefacades.order.data.CartData;
import de.hybris.platform.core.model.user.CustomerModel;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;

public class IdealSubscriptionHandler implements PaymentMethodHandler {
@Override
public boolean canHandle(String paymentMethod) {
return Arrays.stream(IdealDetails.TypeEnum.values()).map(IdealDetails.TypeEnum::toString).anyMatch(type -> type.equalsIgnoreCase(paymentMethod));

}

@Override
public void updatePaymentRequest(PaymentRequest paymentRequest, CartData cartData, RecurringContractMode recurringContractMode, CustomerModel customerModel, Boolean is3DS2Allowed, Boolean guestUserTokenizationEnabled) {
if (StringUtils.isNotEmpty(cartData.getAdyenSelectedReference())) {


SepaDirectDebitDetails sepaDirectDebitDetails = new SepaDirectDebitDetails();


sepaDirectDebitDetails.setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
CheckoutPaymentMethod checkoutPaymentMethod = new CheckoutPaymentMethod();
checkoutPaymentMethod.setActualInstance(sepaDirectDebitDetails);
paymentRequest.setPaymentMethod(checkoutPaymentMethod);

paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);

paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.CONTAUTH);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.adyen.commerce.services.impl;

import com.adyen.model.checkout.CheckoutPaymentMethod;
import com.adyen.model.checkout.KlarnaDetails;
import com.adyen.model.checkout.PaymentRequest;
import com.adyen.v6.enums.RecurringContractMode;
import de.hybris.platform.commercefacades.order.data.CartData;
import de.hybris.platform.core.model.user.CustomerModel;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Objects;

public class KlarnaSubscriptionHandler implements PaymentMethodHandler {
@Override
public boolean canHandle(String paymentMethod) {
return Arrays.stream(KlarnaDetails.TypeEnum.values()).map(KlarnaDetails.TypeEnum::toString).anyMatch(type -> type.equalsIgnoreCase(paymentMethod));
}

@Override
public void updatePaymentRequest(PaymentRequest paymentRequest, CartData cartData, RecurringContractMode recurringContractMode, CustomerModel customerModel, Boolean is3DS2Allowed, Boolean guestUserTokenizationEnabled) {
if (StringUtils.isNotEmpty(cartData.getAdyenSelectedReference())) {

KlarnaDetails klarnaDetails;

if (Objects.isNull(paymentRequest.getPaymentMethod()) || Objects.isNull(paymentRequest.getPaymentMethod().getKlarnaDetails())) {
klarnaDetails = new KlarnaDetails();
} else {
klarnaDetails = paymentRequest.getPaymentMethod().getKlarnaDetails();
}

klarnaDetails.setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
CheckoutPaymentMethod checkoutPaymentMethod = new CheckoutPaymentMethod();
checkoutPaymentMethod.setActualInstance(klarnaDetails);
paymentRequest.setPaymentMethod(checkoutPaymentMethod);

paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);

paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.CONTAUTH);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.adyen.commerce.services.impl;

import com.adyen.model.checkout.CheckoutPaymentMethod;
import com.adyen.model.checkout.PayPalDetails;
import com.adyen.model.checkout.PaymentRequest;
import com.adyen.v6.enums.RecurringContractMode;
import de.hybris.platform.commercefacades.order.data.CartData;
import de.hybris.platform.core.model.user.CustomerModel;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Objects;

public class PayPalSubscriptionHandler implements PaymentMethodHandler {
@Override
public boolean canHandle(String paymentMethod) {
return Arrays.stream(PayPalDetails.TypeEnum.values()).map(PayPalDetails.TypeEnum::toString).anyMatch(type -> type.equalsIgnoreCase(paymentMethod));
}

@Override
public void updatePaymentRequest(PaymentRequest paymentRequest, CartData cartData, RecurringContractMode recurringContractMode, CustomerModel customerModel, Boolean is3DS2Allowed, Boolean guestUserTokenizationEnabled) {
if (StringUtils.isNotEmpty(cartData.getAdyenSelectedReference())) {

PayPalDetails payPalDetails;

if (Objects.isNull(paymentRequest.getPaymentMethod()) || Objects.isNull(paymentRequest.getPaymentMethod().getPayPalDetails())) {
payPalDetails = new PayPalDetails();
} else {
payPalDetails = paymentRequest.getPaymentMethod().getPayPalDetails();
}

payPalDetails.setStoredPaymentMethodId(cartData.getAdyenSelectedReference());
CheckoutPaymentMethod checkoutPaymentMethod = new CheckoutPaymentMethod();
checkoutPaymentMethod.setActualInstance(payPalDetails);
paymentRequest.setPaymentMethod(checkoutPaymentMethod);

paymentRequest.setRecurringProcessingModel(PaymentRequest.RecurringProcessingModelEnum.SUBSCRIPTION);

paymentRequest.setShopperInteraction(PaymentRequest.ShopperInteractionEnum.CONTAUTH);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* Factory for creating appropriate payment method handlers
Expand All @@ -19,16 +19,20 @@ public PaymentMethodHandlerFactory() {
new CreditCardPaymentHandler(),
new OneClickPaymentHandler(),
new SchemePaymentHandler(),
new AlternativePaymentHandler()
new AlternativePaymentHandler(),
new CreditCardSubscriptionHandler(),
new IdealSubscriptionHandler(),
new KlarnaSubscriptionHandler(),
new PayPalSubscriptionHandler()
);
}

/**
* Gets the appropriate handler for the given payment method
*/
public Optional<PaymentMethodHandler> getHandler(String paymentMethod) {
public List<PaymentMethodHandler> getHandler(String paymentMethod) {
return handlers.stream()
.filter(handler -> handler.canHandle(paymentMethod))
.findFirst();
.collect(Collectors.toUnmodifiableList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ public enum StorefrontType {
SPA("spa"),
SPARTACUS("spartacus"),
CUSTOM("custom"),
EXPRESSOCC("expressocc");
EXPRESSOCC("expressocc"),
SUBSCRIPTION("subscription");

private final String value;

Expand Down
16 changes: 16 additions & 0 deletions adyenv6core/src/com/adyen/v6/events/TokenizationEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.adyen.v6.events;

import com.adyen.commerce.data.TokenWebhookRequestData;
import de.hybris.platform.servicelayer.event.events.AbstractEvent;

public class TokenizationEvent extends AbstractEvent {
private final TokenWebhookRequestData data;

public TokenizationEvent(final TokenWebhookRequestData data) {
this.data = data;
}

public TokenWebhookRequestData getData() {
return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.adyen.v6.listeners;

import com.adyen.commerce.data.TokenWebhookRequestData;
import com.adyen.v6.events.TokenizationEvent;
import com.adyen.v6.repository.PaymentTransactionRepository;
import de.hybris.platform.core.model.order.AbstractOrderModel;
import de.hybris.platform.core.model.order.payment.PaymentInfoModel;
import de.hybris.platform.core.model.user.CustomerModel;
import de.hybris.platform.payment.model.PaymentTransactionModel;
import de.hybris.platform.servicelayer.event.impl.AbstractEventListener;
import de.hybris.platform.servicelayer.model.ModelService;
import org.apache.commons.lang.NotImplementedException;
import org.apache.log4j.Logger;

import java.util.Objects;


public class TokenizationWebhookEventListener extends AbstractEventListener<TokenizationEvent> {
private static final Logger LOG = Logger.getLogger(TokenizationWebhookEventListener.class);

private PaymentTransactionRepository paymentTransactionRepository;
private ModelService modelService;

protected static String TOKEN_CREATED = "recurring.token.created";

public TokenizationWebhookEventListener() {
super();
}

@Override
protected void onEvent(TokenizationEvent tokenizationEvent) {
LOG.debug("Processing Tokenization event");

TokenWebhookRequestData data = tokenizationEvent.getData();
PaymentTransactionModel transactionModel = paymentTransactionRepository.getTransactionModel(data.getEventId());
if (Objects.isNull(transactionModel)) {
throw new IllegalStateException("No PaymentTransactionModel found for eventId(pspReference): " + data.getEventId());
}

AbstractOrderModel order = transactionModel.getOrder();

if (Objects.isNull(order)) {
throw new IllegalStateException("No Order connected to PaymentTransaction with code: " + data.getEventId());
}

crosscheckWithOrder(order, data);

if (TOKEN_CREATED.equals(data.getEventType())) {
PaymentInfoModel paymentInfo = order.getPaymentInfo();
paymentInfo.setAdyenSelectedReference(data.getStoredPaymentMethodId());
modelService.save(paymentInfo);
} else {
throw new NotImplementedException("TokenizationWebhookEventListener not implemented for type " + data.getEventType());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Throwing NotImplementedException inside an Event Listener is risky. If the event system retries.

Suggested change
throw new NotImplementedException("TokenizationWebhookEventListener not implemented for type " + data.getEventType());
LOG.warn("Received Tokenization event of type [" + data.getEventType() + "] which is not currently handled.");

}

}

protected void crosscheckWithOrder(final AbstractOrderModel order, final TokenWebhookRequestData tokenWebhookRequestData) {
boolean validationResult = true;
validationResult &= ((CustomerModel) order.getPaymentInfo().getUser()).getCustomerID().equals(tokenWebhookRequestData.getShopperReference());

validationResult &= order.getStore().getAdyenMerchantAccount().equals(tokenWebhookRequestData.getMerchantAccount());

validationResult &= (order.getStore().getAdyenTestMode() && "test".equals(tokenWebhookRequestData.getEnvironment())) ||
(!order.getStore().getAdyenTestMode() && "live".equals(tokenWebhookRequestData.getEnvironment()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

The strings "test" and "live" are hardcoded in the cross-check logic.
Move these to constants in AdyenConstants or a similar configuration class to ensure consistency across the codebase.


if (!validationResult) {
throw new IllegalArgumentException("Token webhook request is not valid. EventId (pspReference): " + tokenWebhookRequestData.getEventId() +
" type: " + tokenWebhookRequestData.getEventType() + " shopperReference: " + tokenWebhookRequestData.getShopperReference() +
" createdAt: " + tokenWebhookRequestData.getCreatedAt());
}

}


public void setPaymentTransactionRepository(PaymentTransactionRepository paymentTransactionRepository) {
this.paymentTransactionRepository = paymentTransactionRepository;
}

public void setModelService(ModelService modelService) {
this.modelService = modelService;
}
}
Loading