Skip to content
This repository has been archived by the owner on May 24, 2023. It is now read-only.

Add support for prefix productName for paypal item #166

Merged
merged 16 commits into from
Mar 30, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![Build Status](https://travis-ci.org/commercetools/commercetools-paypal-plus-integration.svg?branch=master)](https://travis-ci.org/commercetools/commercetools-paypal-plus-integration)

# commercetools _Paypal Plus_ Integration Service

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -224,4 +226,4 @@ Additionally, response can contain additional response body. All fields of the r
1. paypal approves the payment for 10€, but the real total amount of sold (shipped) items has changed to 20€

**Possible solution:** the backend has to compare total amount of the payment and total amount of payment's cart before calling `execute/payments` endpoint.
In case of differences, the whole payment process must be restarted.
In case of differences, the whole payment process must be restarted.
9 changes: 9 additions & 0 deletions docs/MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,12 @@ to fallback to default description with payment reference:

**Note**: according to [PayPal Plus documentation](https://developer.paypal.com/docs/api/payments/#definition-transaction)
length of the description must be up to 127 characters, thus length of `reference` must be up to 116 characters.

### To v0.8.0

New service version is capable of adding prefix for product name of paypal `Item`. Prefix is constructed
through a product attribute. Attribute can be configured through `prefixProductNameWithAttr` optional
configuration property in application.yml file.

**Note**: according to [PayPal Plus documentation](https://developer.paypal.com/docs/api/payments/v1/#definition-item)
length of the product name must be up to 127 characters.
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@
import com.commercetools.model.CtpPaymentWithCart;
import com.commercetools.payment.constants.CtpToPaypalPlusPaymentMethodsMapping;
import com.paypal.api.ApplicationContext;
import com.paypal.api.payments.*;
import com.paypal.api.payments.Amount;
import com.paypal.api.payments.Details;
import com.paypal.api.payments.Item;
import com.paypal.api.payments.ItemList;
import com.paypal.api.payments.Payer;
import com.paypal.api.payments.PayerInfo;
import com.paypal.api.payments.Payment;
import com.paypal.api.payments.PaymentEx;
import com.paypal.api.payments.RedirectUrls;
import com.paypal.api.payments.ShippingAddress;
import com.paypal.api.payments.Transaction;
import io.sphere.sdk.cartdiscounts.DiscountedLineItemPriceForQuantity;
import io.sphere.sdk.carts.CustomLineItem;
import io.sphere.sdk.carts.LineItem;
import io.sphere.sdk.models.LocalizedString;
import org.apache.commons.lang3.StringUtils;
import org.javamoney.moneta.Money;

import javax.annotation.Nonnull;
Expand All @@ -34,13 +44,16 @@ public abstract class BasePaymentMapperImpl implements PaymentMapper {
protected final PaypalPlusFormatter paypalPlusFormatter;
protected final CtpToPaypalPlusPaymentMethodsMapping ctpToPpPaymentMethodsMapping;
protected final AddressMapper addressMapper;
protected final ProductNameMapper productNameMapper;

public BasePaymentMapperImpl(@Nonnull PaypalPlusFormatter paypalPlusFormatter,
@Nonnull CtpToPaypalPlusPaymentMethodsMapping ctpToPpPaymentMethodsMapping,
@Nonnull AddressMapper addressMapper) {
@Nonnull AddressMapper addressMapper,
@Nonnull ProductNameMapper productNameMapper) {
this.paypalPlusFormatter = paypalPlusFormatter;
this.ctpToPpPaymentMethodsMapping = ctpToPpPaymentMethodsMapping;
this.addressMapper = addressMapper;
this.productNameMapper = productNameMapper;
}

@Override
Expand Down Expand Up @@ -212,15 +225,18 @@ protected List<Item> getLineItems(@Nonnull CtpPaymentWithCart paymentWithCartLik
* @see #mapCustomLineItemToPaypalPlusItem(CustomLineItem, List)
*/
private Stream<Item> mapLineItemToPaypalPlusItem(@Nonnull LineItem lineItem, @Nonnull List<Locale> locales) {
final String paypalItemName = productNameMapper.getPaypalItemName(lineItem, locales);
Stream<Item> paypalItemStream;
if (lineItem.getDiscountedPricePerQuantity().size() > 0) {
return lineItem.getDiscountedPricePerQuantity().stream()
.map(dlipfq -> createPaypalPlusItem(lineItem.getName(), locales, dlipfq))
.map(item -> item.setSku(lineItem.getVariant().getSku()));
paypalItemStream = lineItem.getDiscountedPricePerQuantity().stream()
.map(dlipfq -> createPaypalPlusItem(paypalItemName, dlipfq));
} else {
MonetaryAmount actualLineItemPrice = lineItem.getPrice().getValue();
paypalItemStream = Stream.of(
createPaypalPlusItem(paypalItemName, lineItem.getQuantity(), actualLineItemPrice));
}

MonetaryAmount actualLineItemPrice = lineItem.getPrice().getValue();
return Stream.of(createPaypalPlusItem(lineItem.getName(), locales, lineItem.getQuantity(), actualLineItemPrice))
.map(paypalItem -> this.plusSkuProductName(lineItem, locales, paypalItem));
return paypalItemStream.map(paypalItem -> paypalItem.setSku(lineItem.getVariant().getSku()));
}

/**
Expand All @@ -238,29 +254,32 @@ private Stream<Item> mapLineItemToPaypalPlusItem(@Nonnull LineItem lineItem, @No
* @see #mapCustomLineItemToPaypalPlusItem(CustomLineItem, List)
*/
protected Stream<Item> mapCustomLineItemToPaypalPlusItem(@Nonnull CustomLineItem customLineItem, @Nonnull List<Locale> locales) {
String itemName = customLineItem.getName().get(locales);
if (StringUtils.isBlank(itemName)) {
Locale fallBackLocale = customLineItem.getName().getLocales()
.iterator().next();
butenkor marked this conversation as resolved.
Show resolved Hide resolved
itemName = customLineItem.getName().get(fallBackLocale);
ahmetoz marked this conversation as resolved.
Show resolved Hide resolved
}

if (customLineItem.getDiscountedPricePerQuantity().size() > 0) {
String finalItemName = itemName;
butenkor marked this conversation as resolved.
Show resolved Hide resolved
return customLineItem.getDiscountedPricePerQuantity().stream()
.map(dlipfq -> createPaypalPlusItem(customLineItem.getName(), locales, dlipfq));
.map(dlipfq -> createPaypalPlusItem(finalItemName, dlipfq));
}

MonetaryAmount actualCustomLineItemPrice = customLineItem.getMoney();
return Stream.of(createPaypalPlusItem(customLineItem.getName(), locales,
return Stream.of(createPaypalPlusItem(itemName,
customLineItem.getQuantity(), actualCustomLineItemPrice));
}

protected Item createPaypalPlusItem(@Nonnull LocalizedString itemName, @Nonnull List<Locale> locales,
protected Item createPaypalPlusItem(String itemName,
@Nonnull DiscountedLineItemPriceForQuantity dlipfq) {
return createPaypalPlusItem(itemName, locales, dlipfq.getQuantity(), dlipfq.getDiscountedPrice().getValue());
}

private Item plusSkuProductName(@Nonnull LineItem lineItem, @Nonnull List<Locale> locales, @Nonnull Item paypalItem) {
return paypalItem.setSku(lineItem.getVariant().getSku())
.setName(lineItem.getName().get(locales));
return createPaypalPlusItem(itemName, dlipfq.getQuantity(), dlipfq.getDiscountedPrice().getValue());
}

protected Item createPaypalPlusItem(@Nonnull LocalizedString itemName, @Nonnull List<Locale> locales,
protected Item createPaypalPlusItem(String itemName,
@Nonnull Long quantity, @Nonnull MonetaryAmount price) {
return new Item(itemName.get(locales),
return new Item(itemName,
String.valueOf(quantity),
paypalPlusFormatter.monetaryAmountToString(price),
price.getCurrency().getCurrencyCode());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public class DefaultPaymentMapperImpl extends BasePaymentMapperImpl implements P

@Autowired
public DefaultPaymentMapperImpl(@Nonnull PaypalPlusFormatter paypalPlusFormatter,
@Nonnull AddressMapper addressMapper) {
super(paypalPlusFormatter, DEFAULT, addressMapper);
@Nonnull AddressMapper addressMapper,
@Nonnull ProductNameMapper productNameMapper) {
super(paypalPlusFormatter, DEFAULT, addressMapper, productNameMapper);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public class InstallmentPaymentMapperImpl extends BasePaymentMapperImpl implemen

@Autowired
public InstallmentPaymentMapperImpl(@Nonnull PaypalPlusFormatter paypalPlusFormatter,
@Nonnull AddressMapper addressMapper) {
super(paypalPlusFormatter, INSTALLMENT, addressMapper);
@Nonnull AddressMapper addressMapper,
@Nonnull ProductNameMapper productNameMapper) {
super(paypalPlusFormatter, INSTALLMENT, addressMapper, productNameMapper);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.commercetools.helper.mapper.impl.payment;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import io.sphere.sdk.carts.LineItem;
import io.sphere.sdk.models.EnumValue;
import io.sphere.sdk.models.LocalizedEnumValue;
import io.sphere.sdk.models.LocalizedString;
import io.sphere.sdk.products.ProductVariant;
import io.sphere.sdk.products.attributes.Attribute;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

@Component
public class ProductNameMapper {

protected String prefixProductNameWithAttr;

@Value("${prefixProductNameWithAttr:#{null}}")
public void setPrefixProductNameWithAttr(String prefixProductNameWithAttr) {
this.prefixProductNameWithAttr = prefixProductNameWithAttr;
}

/**
* @param lineItem
* @param locales
* @return productName String that will be passed to paypal
*/
public String getPaypalItemName(@Nonnull LineItem lineItem, @Nonnull List<Locale> locales) {
String productName = lineItem.getName().get(locales);
if (StringUtils.isBlank(productName)) {
Locale fallBackLocale = lineItem.getName().getLocales()
.iterator().next();
productName = lineItem.getName().get(fallBackLocale);
}
ahmetoz marked this conversation as resolved.
Show resolved Hide resolved
if (StringUtils.isNotBlank(prefixProductNameWithAttr)) {
String prefix = getPrefixForProductName(lineItem, locales);
if (StringUtils.isNotBlank(prefix)) {
return prefix + StringUtils.SPACE + productName;
ahmetoz marked this conversation as resolved.
Show resolved Hide resolved
}
}
return productName;
}

private String getPrefixForProductName(@Nonnull LineItem lineItem, @Nonnull List<Locale> locales) {
String prefix = "";
ProductVariant variant = lineItem.getVariant();
if (Objects.nonNull(variant) && Objects.nonNull(variant.getAttributes())) {
Attribute attribute = variant.getAttribute(prefixProductNameWithAttr);
if (Objects.nonNull(attribute)) {
prefix = extractLabelValue(attribute, locales);
}
}
return prefix;
}

private String extractLabelValue(@Nonnull Attribute attribute, @Nonnull List<Locale> locales) {
// set[] attributes are discarded
if (attribute.getValueAsJsonNode() instanceof ArrayNode) {
return "";
}

return getAttributeLabelValue(attribute, locales);
}

private String getAttributeLabelValue(@Nonnull final Attribute attribute, @Nonnull List<Locale> locales) {
JsonNode valueJsonNode = attribute.getValueAsJsonNode();
if (Objects.nonNull(valueJsonNode) &&
valueJsonNode.getNodeType() == JsonNodeType.OBJECT && valueJsonNode.hasNonNull("label")) {
//either enum or lenum
JsonNode labelJsonNode = valueJsonNode.get("label");
if (labelJsonNode.getNodeType() == JsonNodeType.OBJECT) {
// lenum
LocalizedEnumValue localizedEnumValue = attribute.getValueAsLocalizedEnumValue();
return localizedEnumValue.getLabel().get(locales);
} else {
// enum
EnumValue enumValue = attribute.getValueAsEnumValue();
return enumValue.getLabel();
}
} else if (Objects.nonNull(valueJsonNode) &&
valueJsonNode.getNodeType() == JsonNodeType.OBJECT && !valueJsonNode.has("typeId")) {
//lText
LocalizedString localizedString = attribute.getValueAsLocalizedString();
return localizedString.get(locales);
} else {
return attribute.getValueAsString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@
import io.sphere.sdk.client.SphereClient;
import io.sphere.sdk.expansion.ExpansionPath;
import io.sphere.sdk.json.SphereJsonUtils;
import io.sphere.sdk.models.LocalizedEnumValue;
import io.sphere.sdk.models.LocalizedString;
import io.sphere.sdk.payments.Payment;
import io.sphere.sdk.payments.PaymentDraftBuilder;
import io.sphere.sdk.payments.PaymentDraftDsl;
import io.sphere.sdk.payments.commands.PaymentCreateCommand;
import io.sphere.sdk.payments.queries.PaymentByIdGet;
import io.sphere.sdk.products.*;
import io.sphere.sdk.products.attributes.AttributeDefinitionDraft;
import io.sphere.sdk.products.attributes.AttributeDefinitionDraftBuilder;
import io.sphere.sdk.products.attributes.AttributeDefinitionDraftDsl;
import io.sphere.sdk.products.attributes.AttributeDraft;
import io.sphere.sdk.products.attributes.LocalizedEnumAttributeType;
import io.sphere.sdk.products.commands.ProductCreateCommand;
import io.sphere.sdk.producttypes.ProductType;
import io.sphere.sdk.producttypes.ProductTypeDraftBuilder;
Expand All @@ -53,6 +59,8 @@
import java.util.concurrent.CompletionStage;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.commercetools.helper.mapper.PaymentMapper.getApprovalUrl;
import static com.commercetools.payment.constants.ctp.CtpPaymentCustomFields.APPROVAL_URL;
Expand Down Expand Up @@ -138,15 +146,22 @@ protected String createCartAndPayment(@Nonnull SphereClient sphereClient) {
protected CompletionStage<Cart> createCartCS(@Nonnull SphereClient sphereClient) {
CartDraft dummyComplexCartWithDiscounts = CartDraftBuilder.of(getDummyComplexCartDraftWithDiscounts())
.currency(EUR)
.locale(Locale.GERMANY)
.build();
CartDraft cartDraft = getProductsInjectedCartDraft(sphereClient, dummyComplexCartWithDiscounts);

return sphereClient.execute(CartCreateCommand.of(cartDraft));
}

public static CartDraft getProductsInjectedCartDraft(@Nonnull SphereClient sphereClient, CartDraft dummyComplexCartWithDiscounts) {
//Product Type attribute
AttributeDefinitionDraftDsl attributeDefinitionDraftDsl = AttributeDefinitionDraftBuilder.of(LocalizedEnumAttributeType.of(
LocalizedEnumValue.of("TestKey", LocalizedString.ofEnglish("TestAttributeValue"))
), "marke", LocalizedString.ofEnglish("marke"), false).build();
List<AttributeDefinitionDraft> attributeDefinitionDraftList = Stream.of(attributeDefinitionDraftDsl).collect(Collectors.toList());

//Create the product
ProductTypeDraftDsl typeDraftDsl = ProductTypeDraftBuilder.of(UUID.randomUUID().toString(), "testProd01", "testProd01", null).build();
ProductTypeDraftDsl typeDraftDsl = ProductTypeDraftBuilder.of(UUID.randomUUID().toString(), "testProd01", "testProd01", attributeDefinitionDraftList).build();
ProductTypeCreateCommand productTypeCreateCommand = ProductTypeCreateCommand.of(typeDraftDsl);
CompletionStage<Product> productCompletionStage = sphereClient.execute(productTypeCreateCommand)
.thenCompose(productType -> createDummyProduct(sphereClient, productType));
Expand All @@ -163,7 +178,10 @@ private static CompletionStage<Product> createDummyProduct(@Nonnull SphereClient
CompletionStage<TaxCategory> taxCategoryCompletionStage = sphereClient.execute(TaxCategoryCreateCommand.of(test_taxCategory));
TaxCategory taxCategory = taxCategoryCompletionStage.toCompletableFuture().join();

ProductVariantDraftDsl variantDraftDsl = ProductVariantDraftBuilder.of().price(PriceDraft.of(BigDecimal.valueOf(100), EUR)).build();
ProductVariantDraftDsl variantDraftDsl = ProductVariantDraftBuilder.of().price(PriceDraft.of(BigDecimal.valueOf(100), EUR))
.attributes(AttributeDraft.of("marke",
LocalizedEnumValue.of("TestKey", LocalizedString.ofEnglish("TestAttributeValue"))))
.build();

ProductDraftDsl productDraftDsl = ProductDraftBuilder
.of(productType, LocalizedString.ofEnglish("TestProd1"), LocalizedString.ofEnglish(UUID.randomUUID().toString()), Collections.emptyList())
Expand Down Expand Up @@ -257,4 +275,4 @@ protected static void assertCustomFields(com.paypal.api.payments.Payment created

assertThat(getApprovalUrl(createdPpPayment)).contains(returnedApprovalUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.commercetools.Application;
import com.commercetools.model.CtpPaymentWithCart;
import com.commercetools.payment.BasePaymentIT;
import com.paypal.api.payments.Item;
import com.paypal.api.payments.Payer;
import com.paypal.api.payments.Transaction;
import io.sphere.sdk.carts.Cart;
import io.sphere.sdk.carts.commands.CartUpdateCommand;
import io.sphere.sdk.carts.commands.updateactions.AddPayment;
Expand All @@ -24,6 +26,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MvcResult;

import java.util.List;
import java.util.Locale;
import java.util.UUID;

Expand Down Expand Up @@ -111,7 +114,12 @@ public void shouldReturnNewPaypalPaymentId() throws Exception {
com.paypal.api.payments.Payment createdPpPayment = getPpPayment(tenantConfig, ppPaymentId);

assertCustomFields(createdPpPayment, returnedApprovalUrl, ppPaymentId);

// Paypal item name should prefix with `prefixProductNameWithAttr` setting in application.yml
List<Transaction> ppPaymentTransactions = createdPpPayment.getTransactions();
assertThat(ppPaymentTransactions.size()).isEqualTo(1);
List<Item> itemList = ppPaymentTransactions.get(0).getItemList().getItems();
assertThat(itemList.size()).isEqualTo(1);
assertThat(itemList.get(0).getName()).isEqualTo("TestAttributeValue TestProd1");
assertThat(ofNullable(createdPpPayment.getPayer())
.map(Payer::getExternalSelectedFundingInstrumentType)).isEmpty();
}
Expand Down
Loading