Skip to content

Commit

Permalink
Cancel payment action
Browse files Browse the repository at this point in the history
  • Loading branch information
ropczan committed Mar 11, 2024
1 parent dc2b293 commit 96377d2
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 0 deletions.
71 changes: 71 additions & 0 deletions src/Action/CancelAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\Action;

use Answear\Payum\PayU\Enum\OrderStatus;
use Answear\Payum\PayU\Model\Model;
use Answear\Payum\PayU\Request\OrderRequestService;
use Answear\Payum\PayU\Util\PaymentHelper;
use Payum\Core\Action\ActionInterface;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Model\PaymentInterface;
use Payum\Core\Request\Cancel;
use Psr\Log\LoggerInterface;
use Webmozart\Assert\Assert;

class CancelAction implements ActionInterface
{
private const STATUSES = [
OrderStatus::Completed,
OrderStatus::Canceled,
];

public function __construct(
private OrderRequestService $orderRequestService,
private LoggerInterface $logger
) {
}

/**
* @param Cancel $request
*/
public function execute($request): void
{
RequestNotSupportedException::assertSupports($this, $request);

$model = Model::ensureArrayObject($request->getModel());

$payment = PaymentHelper::ensurePayment($request->getFirstModel());
Assert::notNull($payment, 'Payment must be set on cancel action.');
$orderId = PaymentHelper::getOrderId($model, $payment);
Assert::notEmpty($orderId, 'OrderId must be set on cancel action.');

try {
$status = $this->getStatus($model, $payment);
if (in_array($status, self::STATUSES, true)) {
return;
}

$this->orderRequestService->cancel($model->orderId(), PaymentHelper::getConfigKey($model, $payment));
} catch (\Throwable $throwable) {
$this->logger->critical('[PayU] Cannot cancel payment.', ['exception' => $throwable]);
}
}

public function supports($request): bool
{
return
$request instanceof Cancel
&& $request->getModel() instanceof \ArrayAccess
&& $request->getFirstModel() instanceof PaymentInterface;
}

private function getStatus(Model $model, PaymentInterface $payment): OrderStatus
{
$response = $this->orderRequestService->retrieve($model->orderId(), PaymentHelper::getConfigKey($model, $payment));

return $response->orders[0]->status;
}
}
29 changes: 29 additions & 0 deletions src/Request/OrderRequestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Answear\Payum\PayU\Service\ConfigProvider;
use Answear\Payum\PayU\Util\ExceptionHelper;
use Answear\Payum\PayU\ValueObject\Request\OrderRequest;
use Answear\Payum\PayU\ValueObject\Response\OrderCanceledResponse;
use Answear\Payum\PayU\ValueObject\Response\OrderCreatedResponse;
use Answear\Payum\PayU\ValueObject\Response\OrderRetrieveResponse;
use Answear\Payum\PayU\ValueObject\Response\OrderTransactions\ByCreditCard;
Expand Down Expand Up @@ -156,4 +157,32 @@ public function retrieveTransactions(string $orderId, ?string $configKey): array
throw new MalformedResponseException($response ?? [], $throwable);
}
}

/**
* @throws MalformedResponseException
* @throws PayUException
*/
public function cancel(string $orderId, ?string $configKey): OrderCanceledResponse
{
try {
$result = $this->client->payuRequest(
'DELETE',
self::ENDPOINT . $orderId,
$this->client->getAuthorizeHeaders(
AuthType::Basic,
$configKey
)
);
} catch (\Throwable $throwable) {
throw ExceptionHelper::getPayUException($throwable);
}

try {
$response = json_decode($result->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);

return OrderCanceledResponse::fromResponse($response);
} catch (\Throwable $throwable) {
throw new MalformedResponseException($response ?? [], $throwable);
}
}
}
33 changes: 33 additions & 0 deletions src/ValueObject/Response/OrderCanceledResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\ValueObject\Response;

class OrderCanceledResponse
{
public function __construct(
public readonly ResponseStatus $status,
public readonly string $orderId,
public readonly ?string $extOrderId = null
) {
}

public static function fromResponse(array $response): self
{
return new self(
ResponseStatus::fromResponse($response['status']),
$response['orderId'],
$response['extOrderId'] ?? null
);
}

public function toArray(): array
{
return [
'status' => $this->status->toArray(),
'orderId' => $this->orderId,
'extOrderId' => $this->extOrderId,
];
}
}
100 changes: 100 additions & 0 deletions tests/Integration/Action/CancelActionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\Tests\Integration\Action;

use Answear\Payum\PayU\Action\CancelAction;
use Answear\Payum\PayU\Request\OrderRequestService;
use Answear\Payum\PayU\Tests\Payment;
use Answear\Payum\PayU\Tests\Util\FileTestUtil;
use Answear\Payum\PayU\ValueObject\Response\OrderCanceledResponse;
use Answear\Payum\PayU\ValueObject\Response\OrderRetrieveResponse;
use Payum\Core\Request\Cancel;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class CancelActionTest extends TestCase
{
/**
* @test
*/
public function successTest(): void
{
$action = $this->getCancelAction(
OrderCanceledResponse::fromResponse(
FileTestUtil::decodeJsonFromFile(
__DIR__ . '/../Request/data/orderCanceledResponse.json'
)
),
OrderRetrieveResponse::fromResponse(
FileTestUtil::decodeJsonFromFile(
__DIR__ . '/../Request/data/retrieveOrderResponse.json'
)
)
);

$payment = new Payment();
$payment->setDetails(FileTestUtil::decodeJsonFromFile(__DIR__ . '/data/detailsWithOrderId.json'));
$expectedBaseDetails = $this->getExpectedBaseDetails();
self::assertSame($expectedBaseDetails, $payment->getDetails());

$request = new Cancel($payment);
$request->setModel($payment->getDetails());

$action->execute($request);
}

private function getCancelAction(
OrderCanceledResponse $orderCanceledResponse,
OrderRetrieveResponse $retrieveOrderResponse
): CancelAction {
$orderRequestService = $this->createMock(OrderRequestService::class);
$orderRequestService->method('retrieve')
->willReturn($retrieveOrderResponse);

$orderRequestService->method('cancel')
->willReturn($orderCanceledResponse);

$logger = $this->createMock(LoggerInterface::class);
$logger->expects(self::never())
->method('critical');

return new CancelAction($orderRequestService, $logger);
}

private function getExpectedBaseDetails(): array
{
return [
'totalAmount' => 95500,
'firstName' => 'Testy',
'lastName' => 'Mjzykdwmh',
'description' => 'Platnost za objednávku č.: 221214-0026UJ-CZ',
'currencyCode' => 'CZK',
'language' => 'cs',
'validityTime' => 259200,
'buyer' => [
'email' => '[email protected]',
'firstName' => 'Testy',
'lastName' => 'Mjzykdwmh',
'phone' => '+420733999019',
'language' => 'cs',
],
'extOrderId' => '221214-0026UJ-CZ',
'client_email' => '[email protected]',
'client_id' => '124077',
'customerIp' => '10.0.13.152',
'creditCardMaskedNumber' => null,
'status' => 'PENDING',
'payUResponse' => [
'status' => [
'statusCode' => 'SUCCESS',
],
'redirectUri' => 'https://merch-prod.snd.payu.com/pay/?orderId=3MRW8ST2Z6221214GUEST000P01&token=eyJhbGciOiJIUzI1NiJ9.eyJvcmRlcklkIjoiM01SVzhTVDJaNjIyMTIxNEdVRVNUMDAwUDAxIiwicG9zSWQiOiI5eWJZVWFZOSIsImF1dGhvcml0aWVzIjpbIlJPTEVfQ0xJRU5UIl0sInBheWVyRW1haWwiOiJ0ZXN0eS5hdXRvbWF0eWN6bmUrZGVjMTQyMDIyMjMyODUzNDgwMDIzQGFuc3dlYXIuY29tIiwiZXhwIjoxNjcxMzE2Mjk1LCJpc3MiOiJQQVlVIiwiYXVkIjoiYXBpLWdhdGV3YXkiLCJzdWIiOiJQYXlVIHN1YmplY3QiLCJqdGkiOiI3NmYyOGZkMi1jOWIxLTRiYzAtOTM5Zi0xNjQ5NjY0ZWNlZDMifQ.NpmBZw0vQP7WEWQEd-ZhXoyg8oo_eKy8gEyfAjri21g',
'orderId' => '3MRW8ST2Z6221214GUEST000P01',
'extOrderId' => '221214-0026UJ-CZ',
],
'orderId' => '3MRW8ST2Z6221214GUEST000P01',
];
}
}
8 changes: 8 additions & 0 deletions tests/Integration/Action/data/orderCanceledResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"status": {
"statusCode": "SUCCESS",
"statusDesc": "Request processing successful"
},
"orderId": "WZHF5FFDRJ140731GUEST000P01",
"extOrderId": "extOrderId123"
}
45 changes: 45 additions & 0 deletions tests/Integration/Action/data/retrieveOrderResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"orders": [
{
"orderId": "WZHF5FFDRJ140731GUEST000P01",
"extOrderId": "358766",
"orderCreateDate": "2014-10-27T14:58:17.443+01:00",
"notifyUrl": "http://localhost/OrderNotify/",
"customerIp": "127.0.0.1",
"merchantPosId": "145227",
"description": "New order",
"currencyCode": "PLN",
"totalAmount": "3200",
"buyer":{
"email":"[email protected]",
"phone":"111111111",
"firstName":"John",
"lastName":"Doe",
"language":"pl"
},
"status": "NEW",
"products": [
{
"name": "Product1",
"unitPrice": "1000",
"quantity": "1"
},
{
"name": "Product2",
"unitPrice": "2200",
"quantity": "1"
}
]
}
],
"status": {
"statusCode": "SUCCESS",
"statusDesc": "Request processing successful"
},
"properties": [
{
"name": "PAYMENT_ID",
"value": "1234567890"
}
]
}
62 changes: 62 additions & 0 deletions tests/Integration/Request/CancelOrderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\Tests\Integration\Request;

use Answear\Payum\PayU\Enum\ResponseStatusCode;
use Answear\Payum\PayU\Exception\PayUAuthorizationException;
use Answear\Payum\PayU\Request\OrderRequestService;
use Answear\Payum\PayU\Tests\Util\FileTestUtil;
use Answear\Payum\PayU\ValueObject\Response\ResponseStatus;
use GuzzleHttp\Psr7\Response;
use Psr\Log\NullLogger;

class CancelOrderTest extends AbstractRequestTestCase
{
/**
* @test
*/
public function createTest(): void
{
$this->mockGuzzleResponse(
new Response(200, [], FileTestUtil::getFileContents(__DIR__ . '/data/orderCanceledResponse.json'))
);

$orderId = 'WZHF5FFDRJ140731GUEST000P01';
$response = $this->getOrderRequestService()->cancel($orderId, null);

self::assertEquals(
new ResponseStatus(
ResponseStatusCode::Success,
'Request processing successful'
),
$response->status
);
self::assertSame($orderId, $response->orderId);
self::assertSame('extOrderId123', $response->extOrderId);
}

/**
* @test
*/
public function createUnauthorizedTest(): void
{
$this->mockGuzzleResponse(
new Response(401, [], FileTestUtil::getFileContents(__DIR__ . '/data/orderUnauthorizedResponse.json'))
);

$this->expectException(PayUAuthorizationException::class);
$this->expectExceptionCode(401);
$this->getOrderRequestService()->cancel('WZHF5FFDRJ140731GUEST000P01', null);
}

private function getOrderRequestService(): OrderRequestService
{
return new OrderRequestService(
$this->getConfigProvider(),
$this->getClient(),
new NullLogger()
);
}
}
8 changes: 8 additions & 0 deletions tests/Integration/Request/data/orderCanceledResponse.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"status": {
"statusCode": "SUCCESS",
"statusDesc": "Request processing successful"
},
"orderId": "WZHF5FFDRJ140731GUEST000P01",
"extOrderId": "extOrderId123"
}

0 comments on commit 96377d2

Please sign in to comment.