Skip to content

Commit

Permalink
Merge branch 'main' into bugfix/remove-invoice-send-without-config-check
Browse files Browse the repository at this point in the history
  • Loading branch information
candemiralp authored Dec 24, 2024
2 parents 47718da + 10722e4 commit eeef061
Show file tree
Hide file tree
Showing 35 changed files with 1,033 additions and 348 deletions.
2 changes: 1 addition & 1 deletion .github/docker-compose.e2e.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3'
services:
playwright:
image: mcr.microsoft.com/playwright:v1.47.2
image: mcr.microsoft.com/playwright:v1.49.1
shm_size: 1gb
ipc: host
cap_add:
Expand Down
2 changes: 2 additions & 0 deletions Gateway/Http/Client/TransactionRefund.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public function placeRequest(TransferInterface $transferObject): array
$this->adyenHelper->logResponse($responseData);
} catch (AdyenException $e) {
$this->adyenHelper->logAdyenException($e);
$responseData['error'] = $e->getMessage();
$responseData['errorCode'] = $e->getAdyenErrorCode();
}
$responses[] = $responseData;
}
Expand Down
163 changes: 109 additions & 54 deletions Gateway/Request/AdditionalDataLevel23DataBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Adyen Payment Module
*
* Copyright (c) 2022 Adyen N.V.
* Copyright (c) 2024 Adyen N.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*
Expand All @@ -16,90 +16,145 @@
use Adyen\Payment\Helper\Config;
use Adyen\Payment\Helper\Data;
use Adyen\Payment\Helper\Requests;
use Adyen\Payment\Logger\AdyenLogger;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Payment\Gateway\Helper\SubjectReader;
use Magento\Payment\Gateway\Request\BuilderInterface;
use Magento\Sales\Api\Data\OrderItemInterface;
use Magento\Sales\Model\Order;
use Magento\Sales\Model\Order\Item;
use Magento\Store\Model\StoreManagerInterface;

class AdditionalDataLevel23DataBuilder implements BuilderInterface
{
const ENHANCED_SCHEME_DATA_PREFIX = 'enhancedSchemeData';
const ITEM_DETAIL_LINE_PREFIX = 'itemDetailLine';
const UNIT_OF_MEASURE_PCS = 'pcs';

/**
* @var Config
*/
private $config;
/**
* @var StoreManagerInterface
*/
private $storeManager;
/**
* @var Data
*/
private $adyenHelper;
/**
* @var ChargedCurrency
* @param Config $config
* @param StoreManagerInterface $storeManager
* @param ChargedCurrency $chargedCurrency
* @param Requests $adyenRequestHelper
* @param Data $adyenHelper
* @param AdyenLogger $adyenLogger
*/
private $chargedCurrency;
public function __construct(
public Config $config,
public StoreManagerInterface $storeManager,
public ChargedCurrency $chargedCurrency,
public Requests $adyenRequestHelper,
public Data $adyenHelper,
public AdyenLogger $adyenLogger
) { }

/**
* @var Requests
* This data builder creates `additionalData` object for Level 2/3 enhanced scheme data.
* For more information refer to https://docs.adyen.com/payment-methods/cards/enhanced-scheme-data/l2-l3
*
* @param array $buildSubject
* @return array|array[]
* @throws NoSuchEntityException
*/
private $adyenRequestHelper;

public function __construct(
Config $config,
StoreManagerInterface $storeManager,
ChargedCurrency $chargedCurrency,
Requests $adyenRequestHelper,
Data $adyenHelper
)
public function build(array $buildSubject): array
{
$this->config = $config;
$this->storeManager = $storeManager;
$this->adyenHelper = $adyenHelper;
$this->chargedCurrency = $chargedCurrency;
$this->adyenRequestHelper = $adyenRequestHelper;
}
$request = [];

public function build(array $buildSubject)
{
$requestBody = [];
if ($this->config->sendLevel23AdditionalData($this->storeManager->getStore()->getId())) {
$paymentDataObject = SubjectReader::readPayment($buildSubject);
$payment = $paymentDataObject->getPayment();

/** @var Order $order */
$order = $payment->getOrder();
$currencyCode = $this->chargedCurrency->getOrderAmountCurrency($order)->getCurrencyCode();

$prefix = 'enhancedSchemeData';
$requestBody['additionalData'][$prefix . '.totalTaxAmount'] = $this->adyenHelper->formatAmount($order->getTaxAmount(), $currencyCode);
$requestBody['additionalData'][$prefix . '.customerReference'] = $this->adyenRequestHelper->getShopperReference($order->getCustomerId(), $order->getIncrementId());
// `totalTaxAmount` field is required and L2/L3 data can not be generated without this field.
if (empty($order->getTaxAmount()) || $order->getTaxAmount() < 0 || $order->getTaxAmount() === 0) {
$this->adyenLogger->warning(__('L2/L3 data can not be generated if tax amount is zero.'));
return $request;
}

$additionalDataLevel23 = [
self::ENHANCED_SCHEME_DATA_PREFIX . '.orderDate' => date('dmy', time()),
self::ENHANCED_SCHEME_DATA_PREFIX . '.customerReference' =>
$this->adyenRequestHelper->getShopperReference($order->getCustomerId(), $order->getIncrementId()),
self::ENHANCED_SCHEME_DATA_PREFIX . '.totalTaxAmount' =>
(string) $this->adyenHelper->formatAmount($order->getTaxAmount(), $currencyCode)
];

if ($order->getIsNotVirtual()) {
$requestBody['additionalData'][$prefix . '.freightAmount'] = $this->adyenHelper->formatAmount($order->getBaseShippingAmount(), $currencyCode);
$requestBody['additionalData'][$prefix . '.destinationPostalCode'] = $order->getShippingAddress()->getPostcode();
$requestBody['additionalData'][$prefix . '.destinationCountryCode'] = $order->getShippingAddress()->getCountryId();
$additionalDataLevel23[self::ENHANCED_SCHEME_DATA_PREFIX . '.freightAmount'] =
(string) $this->adyenHelper->formatAmount($order->getBaseShippingAmount(), $currencyCode);

$additionalDataLevel23[self::ENHANCED_SCHEME_DATA_PREFIX . '.destinationPostalCode'] =
$order->getShippingAddress()->getPostcode();

$additionalDataLevel23[self::ENHANCED_SCHEME_DATA_PREFIX . '.destinationCountryCode'] =
$order->getShippingAddress()->getCountryId();

if (!empty($order->getShippingAddress()->getRegionCode())) {
$additionalDataLevel23[self::ENHANCED_SCHEME_DATA_PREFIX . '.destinationStateProvinceCode'] =
$order->getShippingAddress()->getRegionCode();
}
}

$itemIndex = 0;
$itemIndex = 1;
foreach ($order->getItems() as $item) {
/** @var Item $item */
if ($item->getPrice() == 0 && !empty($item->getParentItem())) {
// Products variants get added to the order as separate items, filter out the variants.
if (!$this->validateLineItem($item)) {
continue;
}

$itemPrefix = $prefix . '.itemDetailLine';
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.description'] = $item->getName();
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.unitPrice'] = $this->adyenHelper->formatAmount($item->getPrice(), $currencyCode);
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.discountAmount'] = $this->adyenHelper->formatAmount($item->getDiscountAmount(), $currencyCode);
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.commodityCode'] = $item->getQuoteItemId();
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.quantity'] = $item->getQtyOrdered();
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.productCode'] = $item->getSku();
$requestBody['additionalData'][$itemPrefix . $itemIndex . '.totalAmount'] = $this->adyenHelper->formatAmount($item->getRowTotal(), $currencyCode);
$itemPrefix = self::ENHANCED_SCHEME_DATA_PREFIX . '.' . self::ITEM_DETAIL_LINE_PREFIX;

$additionalDataLevel23[$itemPrefix . $itemIndex . '.description'] = $item->getName();
$additionalDataLevel23[$itemPrefix . $itemIndex . '.discountAmount'] =
(string) $this->adyenHelper->formatAmount($item->getDiscountAmount(), $currencyCode);
$additionalDataLevel23[$itemPrefix . $itemIndex . '.commodityCode'] = (string) $item->getQuoteItemId();
$additionalDataLevel23[$itemPrefix . $itemIndex . '.productCode'] = $item->getSku();
$additionalDataLevel23[$itemPrefix . $itemIndex . '.unitOfMeasure'] = self::UNIT_OF_MEASURE_PCS;
$additionalDataLevel23[$itemPrefix . $itemIndex . '.quantity'] = (string) $item->getQtyOrdered();
$additionalDataLevel23[$itemPrefix . $itemIndex . '.unitPrice'] =
(string) $this->adyenHelper->formatAmount($item->getPrice(), $currencyCode);
$additionalDataLevel23[$itemPrefix . $itemIndex . '.totalAmount'] =
(string) $this->adyenHelper->formatAmount($item->getRowTotal(), $currencyCode);

$itemIndex++;
}

$request = [
'body' => [
'additionalData' => $additionalDataLevel23
]
];
}

return $request;
}

/**
* Required fields `unitPrice`, `totalAmount` or `quantity` can not be null or zero in the line items.
*
* @param OrderItemInterface $orderItem
* @return bool
*/
private function validateLineItem(OrderItemInterface $orderItem): bool
{
$validationResult = true;

// `unitPrice` should be a non-zero numeric value.
if ($orderItem->getPrice() === 0) {
$validationResult = false;
}

// `totalAmount` should be a non-zero numeric value.
if ($orderItem->getRowTotal() === 0) {
$validationResult = false;
}

// `quantity` should be a positive integer. If not, skip the line item.
if ($orderItem->getQtyOrdered() < 1) {
$validationResult = false;
}

return ['body' => $requestBody];
return $validationResult;
}
}
2 changes: 1 addition & 1 deletion Gateway/Request/DescriptionDataBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function build(array $buildSubject)
'Order %1 from %2',
$order->getIncrementId(),
$order->getStore()->getGroup()->getName()
);
)->render();

return $request;
}
Expand Down
9 changes: 9 additions & 0 deletions Model/Sales/OrderRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Magento\Framework\Api\Search\FilterGroupBuilder;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Api\SortOrderBuilder;
use Magento\Framework\Serialize\Serializer\Json as JsonSerializer;
use Magento\Payment\Api\Data\PaymentAdditionalInfoInterfaceFactory;
use Magento\Sales\Api\Data\OrderExtensionFactory;
Expand All @@ -28,13 +29,15 @@
class OrderRepository extends SalesOrderRepository
{
private SearchCriteriaBuilder $searchCriteriaBuilder;
private SortOrderBuilder $sortOrderBuilder;
private FilterBuilder $filterBuilder;
private FilterGroupBuilder $filterGroupBuilder;

public function __construct(
SearchCriteriaBuilder $searchCriteriaBuilder,
FilterBuilder $filterBuilder,
FilterGroupBuilder $filterGroupBuilder,
SortOrderBuilder $sortOrderBuilder,
Metadata $metadata,
SearchResultFactory $searchResultFactory,
CollectionProcessorInterface $collectionProcessor = null,
Expand All @@ -56,6 +59,7 @@ public function __construct(
);

$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->sortOrderBuilder = $sortOrderBuilder;
$this->filterBuilder = $filterBuilder;
$this->filterGroupBuilder = $filterGroupBuilder;
}
Expand All @@ -68,9 +72,14 @@ public function getOrderByQuoteId(int $quoteId): OrderInterface|false
->create();

$quoteIdFilterGroup = $this->filterGroupBuilder->setFilters([$quoteIdFilter])->create();
$sortOrder = $this->sortOrderBuilder->setField('entity_id')
->setDescendingDirection()
->create();

$searchCriteria = $this->searchCriteriaBuilder
->setFilterGroups([$quoteIdFilterGroup])
->setSortOrders([$sortOrder])
->setPageSize(1)
->create();

$orders = $this->getList($searchCriteria)->getItems();
Expand Down
4 changes: 4 additions & 0 deletions Plugin/GraphQlPlaceOrderAddCartId.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public function __construct(
*/
public function afterResolve(PlaceOrder $placeOrder, array $result): array
{
if (!isset($result['order'])) {
return $result;
}

try {
$cart = $this->quoteHelper->getQuoteByOrderIncrementId($result['order']['order_number']);
$maskedId = $this->quoteIdToMaskedQuoteId->execute($cart->getId());
Expand Down
43 changes: 43 additions & 0 deletions Test/Unit/Gateway/Http/Client/TransactionRefundTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Adyen\Service\Checkout\ModificationsApi;
use Magento\Payment\Gateway\Http\TransferInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Adyen\AdyenException;

class TransactionRefundTest extends AbstractAdyenTestCase
{
Expand Down Expand Up @@ -102,4 +103,46 @@ public function testPlaceRequestIncludesHeadersInRequest()
$this->assertCount(1, $responses);
$this->assertArrayHasKey('pspReference', $responses[0]);
}

public function testPlaceRequestHandlesException()
{
$requestBody = [
'amount' => ['value' => 1000, 'currency' => 'EUR'],
'paymentPspReference' => '123456789'
];

$headers = ['idempotencyExtraData' => ['order_id' => '1001']];

$transferObjectMock = $this->createConfiguredMock(TransferInterface::class, [
'getBody' => [$requestBody],
'getHeaders' => $headers,
'getClientConfig' => []
]);

$serviceMock = $this->createMock(ModificationsApi::class);
$adyenClientMock = $this->createMock(Client::class);

$this->adyenHelperMock->method('initializeAdyenClientWithClientConfig')->willReturn($adyenClientMock);
$this->adyenHelperMock->method('initializeModificationsApi')->willReturn($serviceMock);
$this->adyenHelperMock->method('buildRequestHeaders')->willReturn(['custom-header' => 'value']);

$this->idempotencyHelperMock->expects($this->once())
->method('generateIdempotencyKey')
->with($requestBody, $headers['idempotencyExtraData'])
->willReturn('generated_idempotency_key');

$serviceMock->expects($this->once())
->method('refundCapturedPayment')
->willThrowException(new AdyenException());

$this->adyenHelperMock->expects($this->once())
->method('logAdyenException')
->with($this->isInstanceOf(AdyenException::class));

$responses = $this->transactionRefund->placeRequest($transferObjectMock);
$this->assertIsArray($responses);
$this->assertCount(1, $responses);
$this->assertArrayHasKey('error', $responses[0]);
$this->assertArrayHasKey('errorCode', $responses[0]);
}
}
Loading

0 comments on commit eeef061

Please sign in to comment.