diff --git a/src/Action/CaptureAction.php b/src/Action/CaptureAction.php index 8bab9b8..d63aa19 100644 --- a/src/Action/CaptureAction.php +++ b/src/Action/CaptureAction.php @@ -5,6 +5,7 @@ namespace Answear\Payum\PayU\Action; use Answear\Payum\Model\Payment; +use Answear\Payum\PayU\Enum\CardOnFileEnum; use Answear\Payum\PayU\Enum\PayMethodType; use Answear\Payum\PayU\Enum\RecurringEnum; use Answear\Payum\PayU\Exception\PayUException; @@ -17,6 +18,7 @@ use Answear\Payum\PayU\ValueObject\Request\OrderRequest; use Answear\Payum\PayU\ValueObject\Response\OrderCreated\StatusCode; use Answear\Payum\PayU\ValueObject\Response\OrderCreatedResponse; +use Answear\Payum\PayU\ValueObject\Response\OrderTransactions\ByCreditCard; use Payum\Core\Action\ActionInterface; use Payum\Core\Exception\RequestNotSupportedException; use Payum\Core\GatewayAwareInterface; @@ -59,10 +61,15 @@ public function execute($request): void $configKey = PaymentHelper::getConfigKey($model, $firstModel); $orderRequest = $this->prepareOrderRequest($request, $token, $model); + if (RecurringEnum::Standard === $model->recurring()) { $this->setRecurringStandardPayment($orderRequest, $model, $configKey); } + if (CardOnFileEnum::StandardMerchant === $model->cardOnFile()) { + $this->setCardOnFileStandardMerchantPayment($orderRequest, $model, $configKey); + } + $orderCreatedResponse = $this->orderRequestService->create($orderRequest, $configKey); $model->setPayUResponse($orderCreatedResponse); if (StatusCode::Success === $orderCreatedResponse->status->statusCode) { @@ -157,6 +164,36 @@ private function prepareOrderRequest(Capture $request, TokenInterface $token, Mo ); } + private function setCardOnFileStandardMerchantPayment(OrderRequest $orderRequest, Model $model, ?string $configKey): void + { + $payMethods = $this->payMethodsRequestService->retrieveForUser($model->clientEmail(), $model->clientId(), $configKey); + if (empty($payMethods->cardTokens)) { + throw new \InvalidArgumentException('Cannot make this payment. Token for user does not exist.'); + } + + $transactions = $this->orderRequestService->retrieveTransactions($model->orderId(), $configKey); + $cardToken = null; + foreach ($transactions as $transaction) { + if (!$transaction instanceof ByCreditCard) { + continue; + } + + $cardToken = $this->findPreferredToken($payMethods->cardTokens, $transaction->cardData->cardNumberMasked); + if (null !== $cardToken) { + break; + } + } + + if (null === $cardToken) { + throw new \InvalidArgumentException('Cannot make this payment. Token for user does not exist.'); + } + + $orderRequest->setCardOnFile( + $model->cardOnFile() ?? CardOnFileEnum::StandardMerchant, + new PayMethod(PayMethodType::CardToken, $cardToken['value']) + ); + } + private function setRecurringStandardPayment(OrderRequest $orderRequest, Model $model, ?string $configKey): void { $payMethods = $this->payMethodsRequestService->retrieveForUser($model->clientEmail(), $model->clientId(), $configKey); diff --git a/src/ValueObject/Request/OrderRequest.php b/src/ValueObject/Request/OrderRequest.php index b9c9c07..31c0d47 100644 --- a/src/ValueObject/Request/OrderRequest.php +++ b/src/ValueObject/Request/OrderRequest.php @@ -74,4 +74,10 @@ public function setRequiring(?RecurringEnum $recurring, PayMethod $payMethod): v $this->recurring = $recurring?->value; $this->payMethod = $payMethod; } + + public function setCardOnFile(CardOnFileEnum $cardOnFile, PayMethod $payMethod): void + { + $this->cardOnFile = $cardOnFile; + $this->payMethod = $payMethod; + } } diff --git a/src/ValueObject/Response/OrderTransactions/ByCreditCard.php b/src/ValueObject/Response/OrderTransactions/ByCreditCard.php index 346fa18..9c62499 100644 --- a/src/ValueObject/Response/OrderTransactions/ByCreditCard.php +++ b/src/ValueObject/Response/OrderTransactions/ByCreditCard.php @@ -11,7 +11,8 @@ class ByCreditCard implements OrderRetrieveTransactionsResponseInterface public function __construct( public readonly PayMethod $payMethod, public readonly string $paymentFlow, - public readonly ?array $card, + public readonly CardData $cardData, + public readonly array $cardInstallmentProposal, ) { } @@ -25,7 +26,8 @@ public static function fromResponse(array $response): self return new self( PayMethod::fromResponse($response['payMethod']), $response['paymentFlow'], - $response['card'] ?? null, + CardData::fromResponse($response['card']['cardData']), + $response['cardInstallmentProposal'], ); } } diff --git a/src/ValueObject/Response/OrderTransactions/CardData.php b/src/ValueObject/Response/OrderTransactions/CardData.php new file mode 100644 index 0000000..98b6f07 --- /dev/null +++ b/src/ValueObject/Response/OrderTransactions/CardData.php @@ -0,0 +1,42 @@ +execute($capture); } + /** + * @test + */ + public function captureWithCardOnFile(): void + { + $payMethodRequestService = $this->createMock(PayMethodsRequestService::class); + $payMethodRequestService->method('retrieveForUser') + ->willReturn( + PayMethodsResponse::fromResponse( + FileTestUtil::decodeJsonFromFile(__DIR__ . '/data/retrievePayMethodsForUserResponse.json') + ) + ); + + $captureAction = $this->getCaptureAction( + expectedCreateRequest: FileTestUtil::decodeJsonFromFile(__DIR__ . '/data/expectedOrderRequestWithCardOnFile.json'), + details: FileTestUtil::decodeJsonFromFile(__DIR__ . '/data/detailsWithCardOnFileStandardMerchant.json'), + payMethodsRequestService: $payMethodRequestService, + ); + + $payment = new \Answear\Payum\PayU\Tests\Payment(); + $payment->setDetails(FileTestUtil::decodeJsonFromFile(__DIR__ . '/data/detailsWithCardOnFileStandardMerchant.json')); + + $payMethod = new PayMethod( + PayMethodType::Pbl, + 'some value', + 'some authorisation code' + ); + + $captureToken = new Token(); + $capture = new \Answear\Payum\Action\Request\Capture($captureToken, $payment, $payMethod); + + $redirected = false; + try { + $captureAction->execute($capture); + } catch (HttpRedirect $httpRedirect) { + $redirected = true; + self::assertSame('http://redirect-after-create-payment.url', $httpRedirect->getUrl()); + } + + self::assertTrue($redirected); + } + private function getCaptureAction( ?OrderCreatedResponse $response = null, ?array $details = null, - ?array $expectedCreateRequest = null + ?array $expectedCreateRequest = null, + ?MockObject $payMethodsRequestService = null, ): CaptureAction { $response = $response ?? new OrderCreatedResponse( new OrderCreatedStatus( @@ -226,7 +270,7 @@ static function (OrderRequest $createRequest) use ($expectedCreateRequest) { $captureAction = new CaptureAction( $orderRequestService, - $this->createMock(PayMethodsRequestService::class) + $payMethodsRequestService ?? $this->createMock(PayMethodsRequestService::class) ); $notifyToken = $this->createMock(TokenInterface::class); diff --git a/tests/Integration/Action/data/detailsWithCardOnFileStandardMerchant.json b/tests/Integration/Action/data/detailsWithCardOnFileStandardMerchant.json new file mode 100644 index 0000000..c7057cc --- /dev/null +++ b/tests/Integration/Action/data/detailsWithCardOnFileStandardMerchant.json @@ -0,0 +1,32 @@ +{ + "totalAmount": 95500, + "firstName": "Testy", + "lastName": "Mjzykdwmh", + "description": "Platnost za objedn\u00e1vku \u010d.: 221214-0026UJ-CZ", + "currencyCode": "CZK", + "language": "cs", + "validityTime": 259200, + "buyer": { + "email": "test@email-fake.domain", + "firstName": "Testy", + "lastName": "Mjzykdwmh", + "phone": "+420733999019", + "language": "cs" + }, + "extOrderId": "221214-0026UJ-CZ", + "clientEmail": "test@email-fake.domain", + "clientId": "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", + "cardOnFile": "STANDARD_MERCHANT" +} diff --git a/tests/Integration/Action/data/expectedOrderRequestWithCardOnFile.json b/tests/Integration/Action/data/expectedOrderRequestWithCardOnFile.json new file mode 100644 index 0000000..5346114 --- /dev/null +++ b/tests/Integration/Action/data/expectedOrderRequestWithCardOnFile.json @@ -0,0 +1,37 @@ +{ + "extOrderId": "221214-0026UJ-CZ", + "notifyUrl": "http:\/\/notify.url", + "customerIp": "10.0.13.152", + "merchantPosId": "posId", + "validityTime": 259200, + "description": "Platnost za objedn\u00e1vku \u010d.: 221214-0026UJ-CZ", + "additionalDescription": null, + "visibleDescription": null, + "statementDescription": null, + "currencyCode": "CZK", + "totalAmount": 95500, + "continueUrl": null, + "buyer": { + "email": "test@email-fake.domain", + "firstName": "Testy", + "lastName": "Mjzykdwmh", + "phone": "+420733999019", + "customerId": null, + "extCustomerId": null, + "nin": null, + "language": "cs", + "delivery": null + }, + "products": [ + { + "name": "Platnost za objedn\u00e1vku \u010d.: 221214-0026UJ-CZ", + "unitPrice": 95500, + "quantity": 1, + "virtual": null, + "listingDate": null + } + ], + "payMethods": null, + "cardOnFile": "STANDARD_MERCHANT", + "recurring": null +} diff --git a/tests/Integration/Action/data/retrievePayMethodsForUserResponse.json b/tests/Integration/Action/data/retrievePayMethodsForUserResponse.json new file mode 100644 index 0000000..80ca33e --- /dev/null +++ b/tests/Integration/Action/data/retrievePayMethodsForUserResponse.json @@ -0,0 +1,15 @@ +{ + "cardTokens": [ + { + "status": "ACTIVE", + "cardNumberMasked": "111122*******444" + }, + { + "status": "EXPIRED", + "cardNumberMasked": "666677*******999" + } + ], + "status": { + "statusCode": "SUCCESS" + } +} diff --git a/tests/Integration/Request/OrderRetrieveTransactionsTest.php b/tests/Integration/Request/OrderRetrieveTransactionsTest.php index 27fd627..8fa029a 100644 --- a/tests/Integration/Request/OrderRetrieveTransactionsTest.php +++ b/tests/Integration/Request/OrderRetrieveTransactionsTest.php @@ -9,6 +9,7 @@ use Answear\Payum\PayU\ValueObject\PayMethod; use Answear\Payum\PayU\ValueObject\Response\OrderTransactions\ByCreditCard; use Answear\Payum\PayU\ValueObject\Response\OrderTransactions\ByPBL; +use Answear\Payum\PayU\ValueObject\Response\OrderTransactions\CardData; use GuzzleHttp\Psr7\Response; use Psr\Log\NullLogger; @@ -33,26 +34,30 @@ public function retrieveByCardTest(): void $transaction->getPayMethod() ); self::assertSame('FIRST_ONE_CLICK_CARD', $transaction->paymentFlow); + + $expectedCardData = CardData::fromResponse( + [ + 'cardNumberMasked' => '543402******4014', + 'cardScheme' => 'MC', + 'cardProfile' => 'CONSUMER', + 'cardClassification' => 'DEBIT', + 'cardResponseCode' => '000', + 'cardResponseCodeDesc' => '000 - OK', + 'cardEciCode' => '2', + 'card3DsStatus' => 'Y', + 'card3DsStatusDescription' => 'MessageVersion=2.1.0,browser flow,3DS method not available,dynamic authentication,no cancel indicator,no status reason', + 'cardBinCountry' => 'PL', + 'firstTransactionId' => 'MCC0111LL1121', + ] + ); + + self::assertEquals($expectedCardData, $transaction->cardData); + self::assertSame( [ - 'cardData' => [ - 'cardNumberMasked' => '543402******4014', - 'cardScheme' => 'MC', - 'cardProfile' => 'CONSUMER', - 'cardClassification' => 'DEBIT', - 'cardResponseCode' => '000', - 'cardResponseCodeDesc' => '000 - OK', - 'cardEciCode' => '2', - 'card3DsStatus' => 'Y', - 'card3DsStatusDescription' => 'MessageVersion=2.1.0,browser flow,3DS method not available,dynamic authentication,no cancel indicator,no status reason', - 'cardBinCountry' => 'PL', - 'firstTransactionId' => 'MCC0111LL1121', - ], - 'cardInstallmentProposal' => [ - 'proposalId' => '5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed', - ], + 'proposalId' => '5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed', ], - $transaction->card + $transaction->cardInstallmentProposal ); } diff --git a/tests/Integration/Request/data/orderRetrieveTransactionsByCard.json b/tests/Integration/Request/data/orderRetrieveTransactionsByCard.json index d6f189f..08c7d4d 100644 --- a/tests/Integration/Request/data/orderRetrieveTransactionsByCard.json +++ b/tests/Integration/Request/data/orderRetrieveTransactionsByCard.json @@ -18,10 +18,10 @@ "card3DsStatusDescription": "MessageVersion=2.1.0,browser flow,3DS method not available,dynamic authentication,no cancel indicator,no status reason", "cardBinCountry": "PL", "firstTransactionId": "MCC0111LL1121" - }, - "cardInstallmentProposal": { - "proposalId": "5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed" } + }, + "cardInstallmentProposal": { + "proposalId": "5aff3ba8-0c37-4da1-ba4a-4ff24bcc2eed" } } ]