Skip to content

Commit

Permalink
Cancel payment action (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
ropczan authored Mar 15, 2024
1 parent dc2b293 commit 78a0bf1
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ payum:
payum.action.status: '@Answear\Payum\PayU\Action\StatusAction'
payum.action.convert_payment: '@Answear\Payum\PayU\Action\ConvertPaymentAction'
payum.action.sync_payment: '@Answear\Payum\PayU\Action\SyncPaymentAction'
payum.action.cancel: '@Answear\Payum\PayU\Action\CancelAction'
```

Need to provide all `payum.action` as a service.
Expand Down
64 changes: 64 additions & 0 deletions src/Action/CancelAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\Action;

use Answear\Payum\PayU\Exception\CannotCancelPaymentException;
use Answear\Payum\PayU\Exception\PayUException;
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 Webmozart\Assert\Assert;

class CancelAction implements ActionInterface
{
public function __construct(
private OrderRequestService $orderRequestService,
) {
}

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

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

if (!$this->canCancelPayment($model, $payment)) {
throw new CannotCancelPaymentException('Order status is final, cannot cancel payment.');
}

$this->orderRequestService->cancel($model->orderId(), PaymentHelper::getConfigKey($model, $payment));
}

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

/**
* @throws PayUException
*/
private function canCancelPayment(Model $model, PaymentInterface $payment): bool
{
$response = $this->orderRequestService->retrieve($model->orderId(), PaymentHelper::getConfigKey($model, $payment));

return !$response->orders[0]->status->isFinal();
}
}
13 changes: 13 additions & 0 deletions src/Enum/OrderStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,17 @@ enum OrderStatus: string
case WaitingForConfirmation = 'WAITING_FOR_CONFIRMATION';
case Completed = 'COMPLETED';
case Canceled = 'CANCELED';

public static function finalStatuses(): array
{
return [
self::Completed,
self::Canceled,
];
}

public function isFinal(): bool
{
return in_array($this, self::finalStatuses(), true);
}
}
9 changes: 9 additions & 0 deletions src/Exception/CannotCancelPaymentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Answear\Payum\PayU\Exception;

class CannotCancelPaymentException extends \RuntimeException
{
}
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,
];
}
}
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"
}
]
}
96 changes: 96 additions & 0 deletions tests/Integration/Request/CancelOrderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?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\Exception\PayUNetworkException;
use Answear\Payum\PayU\Exception\PayUServerErrorException;
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);
}

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

$this->expectException(PayUNetworkException::class);
$this->expectExceptionCode(404);
$this->expectExceptionMessageMatches('/DATA_NOT_FOUND/');

$this->getOrderRequestService()->cancel('WZHF5FFDRJ140731GUEST000P01', null);
}

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

$this->expectException(PayUServerErrorException::class);
$this->expectExceptionCode(500);
$this->expectExceptionMessageMatches('/OPENPAYU_ERROR_INTERNAL/');

$this->getOrderRequestService()->cancel('WZHF5FFDRJ140731GUEST000P01', null);
}

private function getOrderRequestService(): OrderRequestService
{
return new OrderRequestService(
$this->getConfigProvider(),
$this->getClient(),
new NullLogger()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"status": {
"statusCode": "OPENPAYU_ERROR_INTERNAL",
"code": "9111",
"codeLiteral": "UNKNOWN_ERROR",
"statusDesc": "Unknown error."
}
}
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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"status": {
"statusCode": "DATA_NOT_FOUND",
"severity": "INFO",
"statusDesc": "Could not find data for given criteria."
}
}
Loading

0 comments on commit 78a0bf1

Please sign in to comment.