From b2830930b5949ace86d772b03aaa0bf08e7e8757 Mon Sep 17 00:00:00 2001 From: Sam Poyigi <6567634+sampoyigi@users.noreply.github.com> Date: Thu, 2 Jan 2025 18:03:00 +0000 Subject: [PATCH] chore: update test scripts for improved coverage Signed-off-by: Sam Poyigi <6567634+sampoyigi@users.noreply.github.com> --- phpstan-baseline.neon | 38 +- src/Classes/AuthorizeNetClient.php | 65 +- .../AuthorizeNetTransactionRequest.php | 16 + src/Classes/BasePaymentGateway.php | 14 +- src/Classes/PayPalClient.php | 44 +- src/Classes/PaymentGateways.php | 16 +- src/Concerns/WithApplicableFee.php | 5 + src/Extension.php | 11 + .../Observers/PaymentProfileObserver.php | 15 + src/Models/Payment.php | 17 +- src/Models/PaymentProfile.php | 7 - src/Payments/AuthorizeNetAim.php | 92 +- src/Payments/Mollie.php | 4 +- src/Payments/PaypalExpress.php | 11 +- src/Payments/Square.php | 30 +- src/Payments/Stripe.php | 36 +- tests/Classes/AuthorizeNetClientTest.php | 113 ++- tests/Classes/BasePaymentGatewayTest.php | 75 +- tests/Classes/PayPalClientTest.php | 13 +- tests/Classes/PaymentGatewaysTest.php | 70 ++ tests/Concerns/WithApplicableFeeTest.php | 11 + tests/Concerns/WithPaymentProfileTest.php | 18 +- tests/ExtensionTest.php | 5 +- .../Models/Observers/PaymentObserverTest.php | 2 +- .../Observers/PaymentProfileObserverTest.php | 18 + tests/Models/PaymentTest.php | 6 +- tests/Payments/AuthorizeNetAimTest.php | 291 ++++-- tests/Payments/MollieTest.php | 272 +++-- tests/Payments/PaypalExpressTest.php | 186 ++-- tests/Payments/SquareTest.php | 583 ++++++++--- tests/Payments/StripeTest.php | 952 +++++++++++++----- 31 files changed, 2161 insertions(+), 875 deletions(-) create mode 100644 src/Classes/AuthorizeNetTransactionRequest.php create mode 100644 src/Models/Observers/PaymentProfileObserver.php create mode 100644 tests/Models/Observers/PaymentProfileObserverTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a408b00..9b6eddb 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -51,14 +51,14 @@ parameters: path: src/Classes/BasePaymentGateway.php - - message: "#^Method Igniter\\\\PayRegister\\\\Classes\\\\BasePaymentGateway\\:\\:initialize\\(\\) should return array but return statement is missing\\.$#" + message: "#^Method Igniter\\\\PayRegister\\\\Classes\\\\BasePaymentGateway\\:\\:getPaymentFormViewName\\(\\) invoked with 1 parameter, 0 required\\.$#" count: 1 path: src/Classes/BasePaymentGateway.php - - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Classes\\\\PayPalClient\\:\\:\\$config\\.$#" + message: "#^Method Igniter\\\\PayRegister\\\\Classes\\\\BasePaymentGateway\\:\\:initialize\\(\\) should return array but return statement is missing\\.$#" count: 1 - path: src/Classes/PayPalClient.php + path: src/Classes/BasePaymentGateway.php - message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\View\\\\Factory\\:\\:getFinder\\(\\)\\.$#" @@ -155,6 +155,11 @@ parameters: count: 2 path: src/Models/Observers/PaymentObserver.php + - + message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\PaymentProfile\\:\\:\\$is_primary\\.$#" + count: 1 + path: src/Models/Observers/PaymentProfileObserver.php + - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:\\$class_name\\.$#" count: 4 @@ -165,11 +170,6 @@ parameters: count: 1 path: src/Models/Payment.php - - - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:\\$model\\.$#" - count: 1 - path: src/Models/Payment.php - - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:\\$payment_id\\.$#" count: 2 @@ -190,21 +190,11 @@ parameters: count: 2 path: src/Models/Payment.php - - - message: "#^Call to an undefined method Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:beforeRenderPaymentForm\\(\\)\\.$#" - count: 1 - path: src/Models/Payment.php - - message: "#^Call to an undefined method Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:getConfigFields\\(\\)\\.$#" count: 1 path: src/Models/Payment.php - - - message: "#^Call to an undefined method Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:getPaymentFormViewName\\(\\)\\.$#" - count: 1 - path: src/Models/Payment.php - - message: "#^Call to an undefined method Igniter\\\\PayRegister\\\\Models\\\\Payment\\:\\:whereIsEnabled\\(\\)\\.$#" count: 1 @@ -290,11 +280,6 @@ parameters: count: 2 path: src/Models/PaymentProfile.php - - - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\PaymentProfile\\:\\:\\$is_primary\\.$#" - count: 1 - path: src/Models/PaymentProfile.php - - message: "#^Access to an undefined property Igniter\\\\PayRegister\\\\Models\\\\PaymentProfile\\:\\:\\$payment_profile_id\\.$#" count: 1 @@ -617,7 +602,7 @@ parameters: - message: "#^Call to an undefined method Igniter\\\\Flame\\\\Database\\\\Model\\:\\:initPaymentProfile\\(\\)\\.$#" - count: 3 + count: 2 path: src/Payments/Stripe.php - @@ -625,11 +610,6 @@ parameters: count: 1 path: src/Payments/Stripe.php - - - message: "#^Call to an undefined static method Illuminate\\\\Support\\\\Facades\\\\Event\\:\\:fire\\(\\)\\.$#" - count: 1 - path: src/Payments/Stripe.php - - message: "#^Method Igniter\\\\PayRegister\\\\Payments\\\\Stripe\\:\\:processPaymentForm\\(\\) should return bool\\|Illuminate\\\\Http\\\\RedirectResponse but return statement is missing\\.$#" count: 1 diff --git a/src/Classes/AuthorizeNetClient.php b/src/Classes/AuthorizeNetClient.php index a8c6c04..a5b28df 100644 --- a/src/Classes/AuthorizeNetClient.php +++ b/src/Classes/AuthorizeNetClient.php @@ -3,19 +3,27 @@ namespace Igniter\PayRegister\Classes; use Igniter\Flame\Exception\ApplicationException; +use net\authorize\api\constants\ANetEnvironment; use net\authorize\api\contract\v1\ANetApiResponseType; -use net\authorize\api\contract\v1\CreateTransactionRequest; +use net\authorize\api\contract\v1\CreditCardType; use net\authorize\api\contract\v1\MerchantAuthenticationType; +use net\authorize\api\contract\v1\OpaqueDataType; +use net\authorize\api\contract\v1\PaymentType; +use net\authorize\api\contract\v1\TransactionRequestType; use net\authorize\api\contract\v1\TransactionResponseType; -use net\authorize\api\controller\CreateTransactionController; class AuthorizeNetClient { + protected bool $sandbox = true; + protected ?MerchantAuthenticationType $authentication = null; - protected ?CreateTransactionRequest $transactionRequest = null; + public function setTestMode(bool $sandbox = true) + { + $this->sandbox = $sandbox; - public function __construct(protected bool $sandbox = false) {} + return $this; + } public function authentication() { @@ -26,23 +34,50 @@ public function authentication() return $this->authentication = new MerchantAuthenticationType; } - public function createTransactionRequest(): CreateTransactionRequest + public function createTransactionRequest(array $fields = []): AuthorizeNetTransactionRequest { - if ($this->transactionRequest) { - return $this->transactionRequest; + $paymentOne = new PaymentType; + $transactionRequestType = new TransactionRequestType; + + if (array_has($fields, ['opaqueDataDescriptor', 'opaqueDataValue'])) { + $opaqueData = new OpaqueDataType; + $opaqueData->setDataDescriptor($fields['opaqueDataDescriptor']); + $opaqueData->setDataValue($fields['opaqueDataValue']); + $paymentOne->setOpaqueData($opaqueData); + } + + if (array_has($fields, ['cardNumber', 'expirationDate'])) { + $creditCard = new CreditCardType; + $creditCard->setCardNumber($fields['cardNumber']); + $creditCard->setExpirationDate($fields['expirationDate']); + $paymentOne = new PaymentType; + $paymentOne->setCreditCard($creditCard); + } + + $transactionRequestType->setPayment($paymentOne); + $transactionRequestType->setTransactionType($fields['transactionType'] ?? ''); + if ($amount = array_get($fields, 'amount')) { + $transactionRequestType->setAmount($amount); + } + + if ($transactionId = array_get($fields, 'transactionId')) { + $transactionRequestType->setRefTransId($transactionId); } - $request = new CreateTransactionRequest; + $request = new AuthorizeNetTransactionRequest; $request->setMerchantAuthentication($this->authentication()); - return $this->transactionRequest = $request; + $request->setRefId($fields['refId'] ?? ''); + $request->setTransactionRequest($transactionRequestType); + + return $request; } - public function createTransaction(CreateTransactionController $controller): TransactionResponseType + public function createTransaction(AuthorizeNetTransactionRequest $request): TransactionResponseType { - $response = $controller->executeWithApiResponse($this->sandbox - ? \net\authorize\api\constants\ANetEnvironment::SANDBOX - : \net\authorize\api\constants\ANetEnvironment::PRODUCTION); + $response = $request->controller()->executeWithApiResponse( + $this->sandbox ? ANetEnvironment::SANDBOX : ANetEnvironment::PRODUCTION, + ); throw_if(is_null($response), new ApplicationException('No response returned')); @@ -53,7 +88,7 @@ public function createTransaction(CreateTransactionController $controller): Tran } if (is_null($transactionResponse) || is_null($transactionResponse->getMessages())) { - throw new ApplicationException($this->getErrorMessageFromResponse($response, $transactionResponse)); + throw new ApplicationException('Transaction failed with empty message'); } return $transactionResponse; @@ -61,7 +96,7 @@ public function createTransaction(CreateTransactionController $controller): Tran protected function getErrorMessageFromResponse(?AnetApiResponseType $response, ?TransactionResponseType $transactionResponse): string { - $message = "Transaction Failed \n Error Code : %s \n Error Message : %s \n"; + $message = "Transaction Failed \n Error Code : %s \n Error Message : %s \n"; if ($transactionResponse != null && $transactionResponse->getErrors() != null) { return sprintf($message, $transactionResponse->getErrors()[0]->getErrorCode(), diff --git a/src/Classes/AuthorizeNetTransactionRequest.php b/src/Classes/AuthorizeNetTransactionRequest.php new file mode 100644 index 0000000..2b10029 --- /dev/null +++ b/src/Classes/AuthorizeNetTransactionRequest.php @@ -0,0 +1,16 @@ +controller ??= new CreateTransactionController($this); + } +} diff --git a/src/Classes/BasePaymentGateway.php b/src/Classes/BasePaymentGateway.php index 47f1bdd..c3e87ab 100644 --- a/src/Classes/BasePaymentGateway.php +++ b/src/Classes/BasePaymentGateway.php @@ -148,11 +148,6 @@ public function completesPaymentOnClient() return false; } - protected function validatePaymentMethod($order, $host) - { - $this->validateApplicableFee($order, $host); - } - /** * Processes payment using passed data. * @@ -170,6 +165,15 @@ public function processPaymentForm($data, $host, $order) */ public function beforeRenderPaymentForm($host, $controller) {} + public function renderPaymentForm() + { + $this->beforeRenderPaymentForm($this->model, controller()); + + $viewName = $this->getPaymentFormViewName($this); + + return view($viewName, ['paymentMethod' => $this->model]); + } + public function getPaymentFormViewName() { $themeCode = resolve(ThemeManager::class)->getActiveThemeCode(); diff --git a/src/Classes/PayPalClient.php b/src/Classes/PayPalClient.php index e0ea75f..734963f 100644 --- a/src/Classes/PayPalClient.php +++ b/src/Classes/PayPalClient.php @@ -8,13 +8,26 @@ class PayPalClient { - public function __construct( - protected ?string $clientId, - protected ?string $clientSecret, - protected bool $sandbox - ) { - throw_unless($this->clientId, ApplicationException::class, 'PayPal client ID is not configured'); - throw_unless($this->clientSecret, ApplicationException::class, 'PayPal client secret is not configured'); + protected ?string $clientId = null; + protected ?string $clientSecret = null; + protected bool $sandbox = false; + + public function setClientId(?string $clientId): PayPalClient + { + $this->clientId = $clientId; + return $this; + } + + public function setClientSecret(?string $clientSecret): PayPalClient + { + $this->clientSecret = $clientSecret; + return $this; + } + + public function setSandbox(bool $sandbox): PayPalClient + { + $this->sandbox = $sandbox; + return $this; } public function getOrder($orderId) @@ -61,6 +74,9 @@ public function refundPayment($id, $params) protected function generateAccessToken() { + throw_unless($this->clientId, ApplicationException::class, 'PayPal client ID is not configured'); + throw_unless($this->clientSecret, ApplicationException::class, 'PayPal client secret is not configured'); + if (!cache()->has('payregister_paypal_access_token')) { $response = Http::asForm() ->withBasicAuth($this->clientId, $this->clientSecret) @@ -80,9 +96,8 @@ protected function generateAccessToken() protected function endpoint(string $uri) { - $endpoint = app()->environment('production') - ? 'https://api-m.paypal.com/' - : 'https://api-m.sandbox.paypal.com/'; + $endpoint = 'https://'; + $endpoint .= app()->environment('production') ? 'api-m.paypal.com/' : 'api-m.sandbox.paypal.com/'; return $endpoint.$uri; } @@ -94,13 +109,4 @@ protected function prepareHeaders(): array 'PayPal-Request-Id' => Str::uuid()->toString(), ]; } - - public function setTestMode(bool $isSandboxMode) - { - $this->config['sandbox'] = $isSandboxMode; - - return $this; - } - - public function setBrandName(mixed $setting) {} } diff --git a/src/Classes/PaymentGateways.php b/src/Classes/PaymentGateways.php index 9fc7192..194b2fd 100644 --- a/src/Classes/PaymentGateways.php +++ b/src/Classes/PaymentGateways.php @@ -65,20 +65,16 @@ public function listGateways() $this->loadGateways(); } - if (!is_array($this->gateways)) { - return []; - } + !is_array($this->gateways) && $this->gateways = []; $result = []; foreach ($this->gateways as $gateway) { - if (!class_exists($gateway['class'])) { - continue; + if (class_exists($gateway['class'])) { + $gatewayObj = new $gateway['class']; + $result[$gateway['code']] = array_merge($gateway, [ + 'object' => $gatewayObj, + ]); } - - $gatewayObj = new $gateway['class']; - $result[$gateway['code']] = array_merge($gateway, [ - 'object' => $gatewayObj, - ]); } return $this->gateways = $result; diff --git a/src/Concerns/WithApplicableFee.php b/src/Concerns/WithApplicableFee.php index 48c0b9d..58a67e1 100644 --- a/src/Concerns/WithApplicableFee.php +++ b/src/Concerns/WithApplicableFee.php @@ -8,6 +8,11 @@ trait WithApplicableFee { + protected function validatePaymentMethod($order, $host) + { + $this->validateApplicableFee($order, $host); + } + protected function validateApplicableFee(Order $order, ?Payment $host = null) { $host = is_null($host) ? $this->model : $host; diff --git a/src/Extension.php b/src/Extension.php index bf1699a..f902992 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -2,15 +2,21 @@ namespace Igniter\PayRegister; +use Igniter\PayRegister\Classes\AuthorizeNetClient; use Igniter\PayRegister\Classes\PaymentGateways; +use Igniter\PayRegister\Classes\PayPalClient; use Igniter\PayRegister\Listeners\CaptureAuthorizedPayment; use Igniter\PayRegister\Listeners\UpdatePaymentIntentSessionOnCheckout; use Igniter\PayRegister\Models\Observers\PaymentObserver; +use Igniter\PayRegister\Models\Observers\PaymentProfileObserver; use Igniter\PayRegister\Models\Payment; +use Igniter\PayRegister\Models\PaymentProfile; use Igniter\PayRegister\Subscribers\FormFieldsSubscriber; use Igniter\System\Classes\BaseExtension; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Event; +use Mollie\Api\MollieApiClient; +use Square\SquareClientBuilder; class Extension extends BaseExtension { @@ -29,10 +35,15 @@ class Extension extends BaseExtension protected $observers = [ Payment::class => PaymentObserver::class, + PaymentProfile::class => PaymentProfileObserver::class, ]; public array $singletons = [ + AuthorizeNetClient::class, + MollieApiClient::class, PaymentGateways::class, + PayPalClient::class, + SquareClientBuilder::class, ]; public function registerPaymentGateways(): array diff --git a/src/Models/Observers/PaymentProfileObserver.php b/src/Models/Observers/PaymentProfileObserver.php new file mode 100644 index 0000000..29c430d --- /dev/null +++ b/src/Models/Observers/PaymentProfileObserver.php @@ -0,0 +1,15 @@ +is_primary && $paymentProfile->wasChanged('is_primary')) { + $paymentProfile->makePrimary(); + } + } +} diff --git a/src/Models/Payment.php b/src/Models/Payment.php index a0eeb09..9906399 100644 --- a/src/Models/Payment.php +++ b/src/Models/Payment.php @@ -90,12 +90,10 @@ public function purgeConfigFields(): array $data = []; $attributes = $this->getAttributes(); foreach ($this->getConfigFields() ?: [] as $name => $config) { - if (!array_key_exists($name, $attributes)) { - continue; + if (array_key_exists($name, $attributes)) { + $data[$name] = $attributes[$name]; + unset($this->attributes[$name]); } - - $data[$name] = $attributes[$name]; - unset($this->attributes[$name]); } return $data; @@ -131,15 +129,6 @@ public function applyGatewayClass($class = null) return !is_null($class); } - public function renderPaymentForm() - { - $this->beforeRenderPaymentForm($this, controller()); - - $viewName = $this->getPaymentFormViewName($this); - - return view($viewName, ['paymentMethod' => $this->model]); - } - public function getGatewayClass() { return $this->class_name; diff --git a/src/Models/PaymentProfile.php b/src/Models/PaymentProfile.php index 67c21bc..86f2903 100644 --- a/src/Models/PaymentProfile.php +++ b/src/Models/PaymentProfile.php @@ -22,13 +22,6 @@ class PaymentProfile extends Model 'is_primary' => 'boolean', ]; - public function afterSave() - { - if ($this->is_primary && $this->wasChanged('is_primary')) { - $this->makePrimary(); - } - } - public function setProfileData($profileData) { $this->profile_data = $profileData; diff --git a/src/Payments/AuthorizeNetAim.php b/src/Payments/AuthorizeNetAim.php index 2a6e711..9ee86f2 100644 --- a/src/Payments/AuthorizeNetAim.php +++ b/src/Payments/AuthorizeNetAim.php @@ -9,12 +9,7 @@ use Igniter\PayRegister\Classes\BasePaymentGateway; use Igniter\PayRegister\Concerns\WithAuthorizedPayment; use Igniter\PayRegister\Concerns\WithPaymentRefund; -use net\authorize\api\contract\v1\CreditCardType; -use net\authorize\api\contract\v1\OpaqueDataType; -use net\authorize\api\contract\v1\PaymentType; -use net\authorize\api\contract\v1\TransactionRequestType; use net\authorize\api\contract\v1\TransactionResponseType; -use net\authorize\api\controller\CreateTransactionController; class AuthorizeNetAim extends BasePaymentGateway { @@ -105,9 +100,7 @@ public function processPaymentForm($data, $host, $order) $response = $this->createAcceptPayment($fields, $order); $responseData = $this->convertResponseToArray($response); - if ($response->getResponseCode() !== '1') { - $order->logPaymentAttempt('Payment unsuccessful -> '.$responseData['description'], 0, $fields, $responseData); - } else { + if ($response->getResponseCode() === '1') { if ($this->shouldAuthorizePayment()) { $order->logPaymentAttempt('Payment authorized', 1, $fields, $responseData); } else { @@ -116,6 +109,8 @@ public function processPaymentForm($data, $host, $order) $order->updateOrderStatus($host->order_status, ['notify' => false]); $order->markAsPaymentProcessed(); + } else { + $order->logPaymentAttempt('Payment unsuccessful -> '.$responseData['description'], 0, $fields, $responseData); } } catch (Exception $ex) { $order->logPaymentAttempt('Payment error -> '.$ex->getMessage(), 0, $fields); @@ -153,7 +148,7 @@ public function processRefundForm($data, $order, $paymentLog) } catch (Exception $ex) { $order->logPaymentAttempt('Refund failed -> '.$ex->getMessage(), 0, $fields, []); - throw new Exception('Refund failed'); + throw new ApplicationException('Refund failed, please try again later or contact system administrator'); } } @@ -169,19 +164,17 @@ public function captureAuthorizedPayment(Order $order) new ApplicationException('Missing payment ID in successful transaction response'), ); - $transactionRequestType = new TransactionRequestType; - $transactionRequestType->setTransactionType('priorAuthCaptureTransaction'); - $transactionRequestType->setRefTransId($paymentId); - $client = $this->createClient(); - $request = $client->createTransactionRequest(); - $request->setRefId($order->hash); - $request->setTransactionRequest($transactionRequestType); + $request = $client->createTransactionRequest([ + 'transactionId' => $paymentId, + 'refId' => $order->hash, + 'transactionType' => 'priorAuthCaptureTransaction', + ]); $this->fireSystemEvent('payregister.authorizenetaim.extendCaptureRequest', [$request], false); - $response = $client->createTransaction(new CreateTransactionController($request)); + $response = $client->createTransaction($request); $responseData = $this->convertResponseToArray($response); if ($response->getResponseCode() == '1') { @@ -205,18 +198,16 @@ public function cancelAuthorizedPayment(Order $order) new ApplicationException('Missing payment ID in successful transaction response'), ); - $transactionRequestType = new TransactionRequestType; - $transactionRequestType->setTransactionType('voidTransaction'); - $transactionRequestType->setRefTransId($paymentId); - $client = $this->createClient(); - $request = $client->createTransactionRequest(); - $request->setRefId($order->hash); - $request->setTransactionRequest($transactionRequestType); + $request = $client->createTransactionRequest([ + 'transactionId' => $paymentId, + 'refId' => $order->hash, + 'transactionType' => 'voidTransaction', + ]); $this->fireSystemEvent('payregister.authorizenetaim.extendCancelRequest', [$request], false); - $response = $client->createTransaction(new CreateTransactionController($request)); + $response = $client->createTransaction($request); $responseData = $this->convertResponseToArray($response); if ($response->getResponseCode() == '1') { @@ -234,7 +225,8 @@ public function cancelAuthorizedPayment(Order $order) protected function createClient() { - $client = new AuthorizeNetClient($this->isTestMode()); + $client = resolve(AuthorizeNetClient::class); + $client->setTestMode($this->isTestMode()); $merchantAuthentication = $client->authentication(); $merchantAuthentication->setName($this->getApiLoginID()); @@ -248,8 +240,8 @@ protected function createClient() protected function getPaymentFormFields($order, $data = []) { return [ + 'refId' => $order->order_id, 'amount' => number_format($order->order_total, 2, '.', ''), - 'transactionId' => $order->order_id, 'currency' => currency()->getUserCurrency(), ]; } @@ -271,54 +263,32 @@ protected function getPaymentRefundFields($order, $data) protected function createAcceptPayment(mixed $fields, Order $order) { - $opaqueData = new OpaqueDataType; - $opaqueData->setDataDescriptor($fields['opaqueDataDescriptor']); - $opaqueData->setDataValue($fields['opaqueDataValue']); - - $transactionRequestType = new TransactionRequestType; - $transactionRequestType->setTransactionType( - $this->shouldAuthorizePayment() ? 'authOnlyTransaction' : 'authCaptureTransaction', - ); - $transactionRequestType->setAmount($fields['amount']); - - $paymentOne = new PaymentType; - $paymentOne->setOpaqueData($opaqueData); - $transactionRequestType->setPayment($paymentOne); - $client = $this->createClient(); - $request = $client->createTransactionRequest(); - $request->setRefId($fields['transactionId']); - $request->setTransactionRequest($transactionRequestType); + $fields['transactionType'] = $this->shouldAuthorizePayment() ? 'authOnlyTransaction' : 'authCaptureTransaction'; + $request = $client->createTransactionRequest($fields); $this->fireSystemEvent('payregister.authorizenetaim.extendAcceptRequest', [$request], false); - return $client->createTransaction(new CreateTransactionController($request)); + return $client->createTransaction($request); } protected function createRefundPayment(mixed $fields, Order $order) { - $creditCard = new CreditCardType; - $creditCard->setCardNumber($fields['card']); - $creditCard->setExpirationDate('XXXX'); - $paymentOne = new PaymentType; - $paymentOne->setCreditCard($creditCard); - - $transactionRequestType = new TransactionRequestType; - $transactionRequestType->setTransactionType('refundTransaction'); - $transactionRequestType->setAmount($fields['amount']); - $transactionRequestType->setPayment($paymentOne); - $transactionRequestType->setRefTransId($fields['transactionId']); - $client = $this->createClient(); - $request = $client->createTransactionRequest(); - $request->setRefId($fields['refId']); - $request->setTransactionRequest($transactionRequestType); + $request = $client->createTransactionRequest([ + 'refId' => $fields['refId'], + 'cardNumber' => $fields['card'], + 'expirationDate' => 'XXXX', + 'transactionType' => 'refundTransaction', + 'amount' => $fields['amount'], + 'transactionId' => $fields['transactionId'], + ]); $this->fireSystemEvent('payregister.authorizenetaim.extendRefundRequest', [$request], false); - return $client->createTransaction(new CreateTransactionController($request)); + return $client->createTransaction($request); } protected function convertResponseToArray(TransactionResponseType $response): array diff --git a/src/Payments/Mollie.php b/src/Payments/Mollie.php index 151e701..b9c89ff 100644 --- a/src/Payments/Mollie.php +++ b/src/Payments/Mollie.php @@ -198,7 +198,7 @@ public function processRefundForm($data, $order, $paymentLog) } catch (Exception $ex) { $order->logPaymentAttempt('Refund failed -> '.$ex->getMessage(), 0, $fields, []); - throw new Exception('Refund failed'); + throw new ApplicationException('Refund failed'); } } @@ -261,7 +261,7 @@ protected function createOrFetchCustomer($profileData, $customer) protected function createClient(): MollieApiClient { - $client = new MollieApiClient; + $client = resolve(MollieApiClient::class); $client->setApiKey($this->getApiKey()); $this->fireSystemEvent('payregister.mollie.extendGateway', [$client]); diff --git a/src/Payments/PaypalExpress.php b/src/Payments/PaypalExpress.php index 4e6d287..3396a63 100644 --- a/src/Payments/PaypalExpress.php +++ b/src/Payments/PaypalExpress.php @@ -57,7 +57,7 @@ public function getTransactionMode(): string */ public function processPaymentForm($data, $host, $order) { - $this->validateApplicableFee($order, $host); + -$this->validateApplicableFee($order, $host); $fields = $this->getPaymentFormFields($order, $data); @@ -173,7 +173,7 @@ public function processRefundForm($data, $order, $paymentLog) try { $response = $this->createClient()->refundPayment($paymentId, $fields); - $message = sprintf('Payment %s refunded successfully -> (%s: %s)', + $message = sprintf('Payment %s refund processed -> (%s: %s)', $paymentId, array_get($data, 'refund_type'), $response->json('id'), ); @@ -182,13 +182,16 @@ public function processRefundForm($data, $order, $paymentLog) } catch (Exception $ex) { $order->logPaymentAttempt('Refund failed -> '.$ex->getMessage(), 0, $fields, []); - throw new Exception('Refund failed'); + throw new ApplicationException('Refund failed'); } } protected function createClient(): PayPalClient { - $client = new PayPalClient($this->getApiUsername(), $this->getApiPassword(), $this->isSandboxMode()); + $client = resolve(PayPalClient::class); + $client->setClientId($this->getApiUsername()); + $client->setClientSecret($this->getApiPassword()); + $client->setSandbox($this->isSandboxMode()); $this->fireSystemEvent('payregister.paypalexpress.extendGateway', [$client]); diff --git a/src/Payments/Square.php b/src/Payments/Square.php index a557b0d..cc06e55 100644 --- a/src/Payments/Square.php +++ b/src/Payments/Square.php @@ -14,7 +14,7 @@ use Square\Environment; use Square\Http\ApiResponse; use Square\Models; -use Square\SquareClient; +use Square\SquareClientBuilder; class Square extends BasePaymentGateway { @@ -167,7 +167,7 @@ public function updatePaymentProfile(Customer $customer, array $data = []): Paym public function deletePaymentProfile(Customer $customer, PaymentProfile $profile) { - $this->handleDeletePaymentProfile($customer, $profile); + return $this->handleDeletePaymentProfile($customer, $profile); } public function payFromPaymentProfile(Order $order, array $data = []) @@ -186,7 +186,7 @@ public function payFromPaymentProfile(Order $order, array $data = []) try { $response = $this->createPayment($fields, $order, $host); - if ($this->handlePaymentResponse($response, $order, $host, $fields)) { + if ($this->handlePaymentResponse($response, $order, $host, $fields, true)) { return; } } catch (Exception $ex) { @@ -267,7 +267,7 @@ protected function createOrFetchCard($customerId, $referenceId, $profileData, $d $errors = $response->getErrors(); $errors = $errors[0]->getDetail(); - throw new ApplicationException('Square Create Payment Card Error '.$errors); + throw new ApplicationException('Square Create Payment Card Error: '.$errors); } } @@ -306,11 +306,11 @@ protected function deletePaymentProfileData($profile) public function processRefundForm($data, $order, $paymentLog) { - if (!is_null($paymentLog->refunded_at) || !is_array($paymentLog->response)) { - throw new ApplicationException('Nothing to refund'); + if (!is_null($paymentLog->refunded_at)) { + throw new ApplicationException('Nothing to refund, payment already refunded'); } - if (array_get($paymentLog->response, 'payment.status') !== 'COMPLETED') { + if (!is_array($paymentLog->response) || array_get($paymentLog->response, 'payment.status') !== 'COMPLETED') { throw new ApplicationException('No charge to refund'); } @@ -344,7 +344,7 @@ public function processRefundForm($data, $order, $paymentLog) $paymentLog->markAsRefundProcessed(); } catch (Exception $e) { logger()->error($e); - $order->logPaymentAttempt('Refund failed: '.$e->getMessage(), 0, $fields, []); + $order->logPaymentAttempt('Refund failed -> '.$e->getMessage(), 0, $fields, []); } } @@ -380,10 +380,10 @@ protected function getPaymentRefundFields($order, $data) */ protected function createClient() { - $client = new SquareClient([ - 'accessToken' => $this->getAccessToken(), - 'environment' => $this->isTestMode() ? Environment::SANDBOX : Environment::PRODUCTION, - ]); + $clientBuilder = resolve(SquareClientBuilder::class); + $clientBuilder->accessToken($this->getAccessToken()); + $clientBuilder->environment($this->isTestMode() ? Environment::SANDBOX : Environment::PRODUCTION); + $client = $clientBuilder->build(); $this->fireSystemEvent('payregister.square.extendGateway', [$client]); @@ -465,10 +465,6 @@ protected function handleUpdatePaymentProfile($customer, $data) protected function handleDeletePaymentProfile($customer, $profile) { - if (!isset($profile->profile_data['customer_id'])) { - return; - } - $cardId = $profile['profile_data']['card_id']; $client = $this->createClient(); $cardsApi = $client->getCardsApi(); @@ -479,7 +475,7 @@ protected function handleDeletePaymentProfile($customer, $profile) $errors = $response->getErrors(); $errors = $errors[0]->getDetail(); - throw new ApplicationException('Square Delete Payment Card Error '.$errors); + throw new ApplicationException('Square Delete Payment Card Error: '.$errors); } $this->deletePaymentProfileData($profile); diff --git a/src/Payments/Stripe.php b/src/Payments/Stripe.php index 445f980..55035f5 100644 --- a/src/Payments/Stripe.php +++ b/src/Payments/Stripe.php @@ -238,9 +238,9 @@ public function captureAuthorizedPayment(Order $order, $data = []) ); if ($response->status == 'succeeded') { - $order->logPaymentAttempt('Payment successful', 1, $data, $response); + $order->logPaymentAttempt('Payment successful', 1, $data, $response, true); } else { - $order->logPaymentAttempt('Payment failed', 0, $data, $response); + $order->logPaymentAttempt('Payment capture failed', 0, $data, $response); } return $response; @@ -335,10 +335,6 @@ public function updatePaymentProfile(Customer $customer, array $data = []): Paym $profileData = array_merge((array)$profile->profile_data, $data); - if (!$profile) { - $profile = $this->model->initPaymentProfile($customer); - } - $profile->card_brand = strtolower(array_get($data, 'card.brand')); $profile->card_last4 = array_get($data, 'card.last4'); $profile->setProfileData($profileData); @@ -348,11 +344,9 @@ public function updatePaymentProfile(Customer $customer, array $data = []): Paym public function deletePaymentProfile(Customer $customer, PaymentProfile $profile) { - if (!isset($profile->profile_data['customer_id'])) { - return; + if (isset($profile->profile_data['customer_id'])) { + $this->createGateway()->customers->delete($profile->profile_data['customer_id'], [], $this->getStripeOptions()); } - - $this->createGateway()->customers->delete($profile->profile_data['customer_id'], [], $this->getStripeOptions()); } public function payFromPaymentProfile(Order $order, array $data = []) @@ -442,11 +436,12 @@ protected function createOrFetchProfileData($customer): array public function processRefundForm($data, $order, $paymentLog) { - if (!is_null($paymentLog->refunded_at) || !is_array($paymentLog->response)) { - throw new ApplicationException('Nothing to refund'); + if (!is_null($paymentLog->refunded_at)) { + throw new ApplicationException('Nothing to refund, payment has already been refunded'); } - if (array_get($paymentLog->response, 'status') !== 'succeeded' + if (!is_array($paymentLog->response) + || array_get($paymentLog->response, 'status') !== 'succeeded' || array_get($paymentLog->response, 'object') !== 'payment_intent' ) { throw new ApplicationException('No charge to refund'); @@ -475,7 +470,7 @@ public function processRefundForm($data, $order, $paymentLog) $paymentLog->markAsRefundProcessed(); } catch (Exception $e) { logger()->error($e); - $order->logPaymentAttempt('Refund failed: '.$e->getMessage(), 0, $fields, []); + $order->logPaymentAttempt('Refund failed -> '.$e->getMessage(), 0, $fields, []); } } @@ -565,8 +560,11 @@ public function processWebhookUrl() return response('Request method must be POST', 400); } - $payload = $this->getWebhookPayload(); + if (!$this->getWebhookSecret()) { + return response('Invalid webhook secret', 400); + } + $payload = $this->getWebhookPayload(); if (!isset($payload['type']) || !strlen($eventType = $payload['type'])) { return response('Missing webhook event name', 400); } @@ -578,7 +576,7 @@ public function processWebhookUrl() $this->$methodName($payload); } - Event::fire('payregister.stripe.webhook.handle'.$eventName, [$payload]); + Event::dispatch('payregister.stripe.webhook.handle'.$eventName, [$payload]); return response('Webhook Handled'); } @@ -601,14 +599,10 @@ protected function webhookHandlePaymentIntentSucceeded($payload) protected function getWebhookPayload(): array { - if (!$webhookSecret = $this->getWebhookSecret()) { - return json_decode(request()->getContent(), true); - } - $event = \Stripe\Webhook::constructEvent( request()->getContent(), request()->header('stripe-signature'), - $webhookSecret, + $this->getWebhookSecret(), ); return $event->toArray(); diff --git a/tests/Classes/AuthorizeNetClientTest.php b/tests/Classes/AuthorizeNetClientTest.php index dc346de..5f9365c 100644 --- a/tests/Classes/AuthorizeNetClientTest.php +++ b/tests/Classes/AuthorizeNetClientTest.php @@ -4,12 +4,14 @@ use Igniter\Flame\Exception\ApplicationException; use Igniter\PayRegister\Classes\AuthorizeNetClient; -use Mockery; +use Igniter\PayRegister\Classes\AuthorizeNetTransactionRequest; use net\authorize\api\contract\v1\ANetApiResponseType; use net\authorize\api\contract\v1\CreateTransactionRequest; use net\authorize\api\contract\v1\MerchantAuthenticationType; use net\authorize\api\contract\v1\MessagesType; use net\authorize\api\contract\v1\TransactionResponseType; +use net\authorize\api\contract\v1\TransactionResponseType\ErrorsAType\ErrorAType; +use net\authorize\api\contract\v1\TransactionResponseType\MessagesAType\MessageAType; use net\authorize\api\controller\CreateTransactionController; beforeEach(function() { @@ -17,71 +19,126 @@ }); it('creates authentication instance', function() { + $this->authorizeNetClient->authentication(); $result = $this->authorizeNetClient->authentication(); expect($result)->toBeInstanceOf(MerchantAuthenticationType::class); }); it('creates transaction request instance', function() { - $result = $this->authorizeNetClient->createTransactionRequest(); + $result = $this->authorizeNetClient->createTransactionRequest([ + 'opaqueDataDescriptor' => 'descriptor', + 'opaqueDataValue' => 'value', + 'transactionType' => 'type', + 'amount' => 100, + ]); expect($result)->toBeInstanceOf(CreateTransactionRequest::class) + ->and($result->controller())->toBeInstanceOf(CreateTransactionController::class) ->and($result->getMerchantAuthentication())->toBeInstanceOf(MerchantAuthenticationType::class); }); -it('returns existing transaction request instance if set', function() { - $result = $this->authorizeNetClient->createTransactionRequest(); - - expect($result)->toBeInstanceOf(CreateTransactionRequest::class); -}); - it('throws exception if no response returned from create transaction', function() { - $request = Mockery::mock(CreateTransactionRequest::class); - $request->shouldReceive('getMerchantAuthentication')->andReturn(Mockery::mock(MerchantAuthenticationType::class)); + $request = mock(AuthorizeNetTransactionRequest::class); + $request->shouldReceive('getMerchantAuthentication')->andReturn(mock(MerchantAuthenticationType::class)); $request->shouldReceive('setClientId')->andReturnSelf(); $request->shouldReceive('jsonSerialize')->andReturn([]); + $controller = mock(CreateTransactionController::class, [$request]); + $controller->shouldReceive('executeWithApiResponse')->andReturnNull(); + $request->shouldReceive('controller')->andReturn($controller); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No response returned'); - $this->authorizeNetClient->createTransaction(new CreateTransactionController($request)); + $this->authorizeNetClient->createTransaction($request); }); it('throws exception if response result code is not Ok', function() { - $request = Mockery::mock(CreateTransactionRequest::class); - $request->shouldReceive('getMerchantAuthentication')->andReturn(Mockery::mock(MerchantAuthenticationType::class)); + $request = mock(AuthorizeNetTransactionRequest::class); + $request->shouldReceive('getMerchantAuthentication')->andReturn(mock(MerchantAuthenticationType::class)); $request->shouldReceive('setClientId')->andReturnSelf(); $request->shouldReceive('jsonSerialize')->andReturn([]); - - $response = Mockery::mock(ANetApiResponseType::class); + $response = mock(ANetApiResponseType::class); $response->shouldReceive('getMessages->getResultCode')->andReturn('Error'); + $transactionResponse = mock(TransactionResponseType::class); + $errorType = mock(ErrorAType::class); + $errorType->shouldReceive('getErrorCode')->andReturn('E00001'); + $errorType->shouldReceive('getErrorText')->andReturn('Failed to create transaction'); + $transactionResponse->shouldReceive('getErrors')->andReturn([$errorType]); + $response->shouldReceive('getTransactionResponse')->andReturn($transactionResponse); + $controller = mock(CreateTransactionController::class, [$request]); + $controller->shouldReceive('executeWithApiResponse')->andReturn($response); + $request->shouldReceive('controller')->andReturn($controller); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessageMatches('/Error Code : E00001/'); + $this->expectExceptionMessageMatches('/Error Message : Failed to create transaction/'); + + $this->authorizeNetClient->createTransaction($request); +}); + +it('throws exception if response result code is not Ok and has message', function() { + $request = mock(AuthorizeNetTransactionRequest::class); + $request->shouldReceive('getMerchantAuthentication')->andReturn(mock(MerchantAuthenticationType::class)); + $request->shouldReceive('setClientId')->andReturnSelf(); + $request->shouldReceive('jsonSerialize')->andReturn([]); + $response = mock(ANetApiResponseType::class); + $transactionResponse = mock(TransactionResponseType::class); + $messagesType = mock(MessagesType::class); + $messageAType = mock(MessageAType::class); + $messageAType->shouldReceive('getCode')->andReturn('E00001'); + $messageAType->shouldReceive('getText')->andReturn('Failed to create transaction'); + $messagesType->shouldReceive('getMessage')->andReturn([$messageAType]); + $messagesType->shouldReceive('getResultCode')->andReturn('Error'); + $transactionResponse->shouldReceive('getErrors')->andReturnNull(); + $response->shouldReceive('getMessages')->andReturn($messagesType); + $response->shouldReceive('getTransactionResponse')->andReturn($transactionResponse); + $controller = mock(CreateTransactionController::class, [$request]); + $controller->shouldReceive('executeWithApiResponse')->andReturn($response); + $request->shouldReceive('controller')->andReturn($controller); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessageMatches('/Error Code : E00001/'); + $this->expectExceptionMessageMatches('/Error Message : Failed to create transaction/'); - $transactionResponse = Mockery::mock(TransactionResponseType::class); + $this->authorizeNetClient->createTransaction($request); +}); + +it('throws exception if response result code is Ok and empty message', function() { + $request = mock(AuthorizeNetTransactionRequest::class); + $request->shouldReceive('getMerchantAuthentication')->andReturn(mock(MerchantAuthenticationType::class)); + $request->shouldReceive('setClientId')->andReturnSelf(); + $request->shouldReceive('jsonSerialize')->andReturn([]); + $response = mock(ANetApiResponseType::class); + $response->shouldReceive('getMessages->getResultCode')->andReturn('Ok'); + $transactionResponse = mock(TransactionResponseType::class); + $transactionResponse->shouldReceive('getMessages')->andReturnNull(); $response->shouldReceive('getTransactionResponse')->andReturn($transactionResponse); + $controller = mock(CreateTransactionController::class, [$request]); + $controller->shouldReceive('executeWithApiResponse')->andReturn($response); + $request->shouldReceive('controller')->andReturn($controller); $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Transaction failed with empty message'); - $this->authorizeNetClient->createTransaction(new CreateTransactionController($request)); + $this->authorizeNetClient->createTransaction($request); }); it('returns transaction response if successful', function() { - $response = Mockery::mock(ANetApiResponseType::class); + $response = mock(ANetApiResponseType::class); $response->shouldReceive('getMessages->getResultCode')->andReturn('Ok'); - - $request = Mockery::mock(CreateTransactionRequest::class); - $request->shouldReceive('getMerchantAuthentication')->andReturn(Mockery::mock(MerchantAuthenticationType::class)); + $request = mock(AuthorizeNetTransactionRequest::class); + $request->shouldReceive('getMerchantAuthentication')->andReturn(mock(MerchantAuthenticationType::class)); $request->shouldReceive('setClientId')->andReturnSelf(); $request->shouldReceive('jsonSerialize')->andReturn([$response]); - - $transactionResponse = Mockery::mock(TransactionResponseType::class); - $transactionResponse->shouldReceive('getMessages')->andReturn(Mockery::mock(MessagesType::class)); + $transactionResponse = mock(TransactionResponseType::class); + $transactionResponse->shouldReceive('getMessages')->andReturn(mock(MessagesType::class)); $response->shouldReceive('getTransactionResponse')->andReturn($transactionResponse); - - $controller = Mockery::mock(CreateTransactionController::class); + $controller = mock(CreateTransactionController::class); $controller->shouldReceive('executeWithApiResponse')->andReturn($response); - app()->instance(CreateTransactionController::class, $controller); + $request->shouldReceive('controller')->andReturn($controller); - $result = $this->authorizeNetClient->createTransaction($controller); + $result = $this->authorizeNetClient->createTransaction($request); expect($result)->toBe($transactionResponse); }); diff --git a/tests/Classes/BasePaymentGatewayTest.php b/tests/Classes/BasePaymentGatewayTest.php index dea6125..d3443e3 100644 --- a/tests/Classes/BasePaymentGatewayTest.php +++ b/tests/Classes/BasePaymentGatewayTest.php @@ -5,12 +5,14 @@ use Igniter\Admin\Models\Status; use Igniter\Cart\Models\Order; use Igniter\Flame\Database\Model; +use Igniter\Main\Classes\MainController; use Igniter\PayRegister\Classes\BasePaymentGateway; +use Igniter\PayRegister\Tests\Payments\Fixtures\TestPayment; use Illuminate\Support\Facades\URL; use Mockery; beforeEach(function() { - $this->model = Mockery::mock(Model::class); + $this->model = Mockery::mock(Model::class)->makePartial(); $this->gateway = new class($this->model) extends BasePaymentGateway { public function defineFieldsConfig() @@ -49,6 +51,18 @@ public function getStatusModel() $gateway->initialize($host); }); +it('defines fields config', function() { + $model = mock(Model::class)->makePartial(); + $gateway = new class($model) extends BasePaymentGateway + { + public function __construct(?Model $model = null) {} + }; + + $result = $gateway->defineFieldsConfig(); + + expect($result)->toEqual('fields'); +}); + it('returns correct config fields', function() { $result = $this->gateway->getConfigFields(); expect($result)->toBe([ @@ -91,6 +105,65 @@ public function getStatusModel() expect($result)->toBe(URL::to('ti_payregister/'.$code)); }); +it('throws exception when processPaymentForm is not implemented', function() { + $data = []; + $host = Mockery::mock(Model::class); + $order = Mockery::mock(Order::class); + + expect(fn() => $this->gateway->processPaymentForm($data, $host, $order))->toThrow(\LogicException::class); +}); + +it('returns null when beforeRenderPaymentForm is not implemented', function() { + $host = Mockery::mock(Model::class); + $controller = MainController::getController(); + + $result = $this->gateway->beforeRenderPaymentForm($host, $controller); + + expect($result)->toBeNull(); +}); + +it('renders payment form', function() { + $controller = MainController::getController(); + $viewName = 'igniter-orange::_partials.payregister.stripe'; + $factory = Mockery::mock(\Illuminate\View\Factory::class); + $factory->shouldReceive('exists')->with($viewName)->andReturnTrue(); + $factory->shouldReceive('make')->with($viewName, ['paymentMethod' => $this->model], [])->andReturnSelf(); + app()->instance('view', $factory); + + $this->model->shouldReceive('getAttribute')->with('code')->andReturn('stripe'); + $this->model->shouldReceive('getAttribute')->with('class_name')->andReturn(TestPayment::class); + + $result = $this->gateway->renderPaymentForm($this->model, $controller); + + expect($result)->toBe($factory); +}); + +it('has payment form blade view under payregister partials folder', function() { + $viewName = 'igniter-orange::_partials.payregister.stripe'; + $factory = Mockery::mock(\Illuminate\View\Factory::class); + $factory->shouldReceive('exists')->with($viewName)->andReturnTrue(); + app()->instance('view', $factory); + + $this->model->shouldReceive('getAttribute')->with('code')->andReturn('stripe'); + $result = $this->gateway->getPaymentFormViewName(); + + expect($result)->toStartWith($viewName); +}); + +it('guesses payment form blade view', function() { + $viewName = 'igniter.payregister::test-payment.payment_form'; + $factory = Mockery::mock(\Illuminate\View\Factory::class); + $factory->shouldReceive('exists')->with($viewName)->andReturnTrue(); + $factory->shouldReceive('exists')->with('igniter-orange::_partials.payregister.test-payment')->andReturnFalse(); + app()->instance('view', $factory); + $this->model->shouldReceive('getAttribute')->with('code')->andReturn('test-payment'); + $this->model->shouldReceive('getAttribute')->with('class_name')->andReturn(TestPayment::class); + + $result = $this->gateway->getPaymentFormViewName(); + + expect($result)->toStartWith($viewName); +}); + it('returns false for completes payment on client', function() { $result = $this->gateway->completesPaymentOnClient(); diff --git a/tests/Classes/PayPalClientTest.php b/tests/Classes/PayPalClientTest.php index c5d1705..49efb45 100644 --- a/tests/Classes/PayPalClientTest.php +++ b/tests/Classes/PayPalClientTest.php @@ -11,7 +11,10 @@ $this->clientId = 'testClientId'; $this->clientSecret = 'testClientSecret'; $this->sandbox = true; - $this->payPalClient = new PayPalClient($this->clientId, $this->clientSecret, $this->sandbox); + $this->payPalClient = new PayPalClient(); + $this->payPalClient->setClientSecret($this->clientSecret); + $this->payPalClient->setClientId($this->clientId); + $this->payPalClient->setSandbox($this->sandbox); }); function mockGenerateAccessToken() @@ -32,14 +35,18 @@ function mockGenerateAccessToken() $this->expectException(ApplicationException::class); $this->expectExceptionMessage('PayPal client ID is not configured'); - new PayPalClient(null, $this->clientSecret, $this->sandbox); + $paypalClient = new PayPalClient(); + $paypalClient->setClientSecret('testClientSecret'); + $paypalClient->getOrder(123); }); it('throws exception if client secret is not configured', function() { $this->expectException(ApplicationException::class); $this->expectExceptionMessage('PayPal client secret is not configured'); - new PayPalClient($this->clientId, null, $this->sandbox); + $paypalClient = new PayPalClient(); + $paypalClient->setClientId('testClientId'); + $paypalClient->getOrder(123); }); it('gets order details successfully', function() { diff --git a/tests/Classes/PaymentGatewaysTest.php b/tests/Classes/PaymentGatewaysTest.php index 1a1b5dc..048c364 100644 --- a/tests/Classes/PaymentGatewaysTest.php +++ b/tests/Classes/PaymentGatewaysTest.php @@ -2,6 +2,9 @@ namespace Igniter\PayRegister\Tests\Classes; +use Igniter\Flame\Support\Facades\File; +use Igniter\Main\Classes\Theme; +use Igniter\Main\Classes\ThemeManager; use Igniter\PayRegister\Classes\PaymentGateways; use Igniter\PayRegister\Models\Payment; use Igniter\PayRegister\Tests\Payments\Fixtures\TestPayment; @@ -54,9 +57,27 @@ public function registerPaymentGateways() ]; } }, + 'test_extension2' => new class + { + }, + 'test_extension3' => new class + { + public function registerPaymentGateways() + { + return 'is-not-array'; + } + }, ]); app()->instance(ExtensionManager::class, $extensionManager); + $this->paymentGateways->registerCallback(function($gateway) { + $gateway->registerGateways('test_owner2', [ + TestPayment::class => [ + 'code' => 'test_gateway2', + ], + ]); + }); + $this->paymentGateways->listGateways(); $result = $this->paymentGateways->findGateway('test_gateway'); @@ -81,3 +102,52 @@ public function registerPaymentGateways() expect($result->getStatusCode())->toBe(403); }); + +it('creates partials returns null when no active theme', function() { + $themeManager = mock(ThemeManager::class); + $themeManager->shouldReceive('getActiveTheme')->andReturnNull(); + app()->instance(ThemeManager::class, $themeManager); + + $result = PaymentGateways::createPartials(); + + expect($result)->toBeNull(); +}); + +it('creates partials for enabled payment methods', function() { + $themeManager = mock(ThemeManager::class); + $theme = mock(Theme::class); + $themeManager->shouldReceive('getActiveTheme')->andReturn($theme); + $theme->shouldReceive('listPartials')->andReturn(collect([])); + $theme->shouldReceive('getPath')->andReturn('/path/to/theme'); + app()->instance(ThemeManager::class, $themeManager); + + Payment::where('status', 1)->update(['status' => 0]); + Payment::factory()->create([ + 'code' => 'test', + 'status' => 1, + 'class_name' => TestPayment::class, + ]); + Payment::factory()->create([ + 'code' => 'test2', + 'status' => 1, + 'class_name' => 'NonExistentPayment', + ]); + + File::shouldReceive('normalizePath')->andReturn(''); + File::shouldReceive('symbolizePath')->andReturn(''); + File::shouldReceive('isFile')->andReturn(true); + File::shouldReceive('getRequire')->andReturn(['fields' => []]); + File::shouldReceive('isLocalPath')->andReturn(false); + File::shouldReceive('isDirectory')->andReturn(false); + File::shouldReceive('makeDirectory')->andReturn(true); + File::shouldReceive('put')->once()->with( + '/path/to/theme/_partials/payregister/testpayment.blade.php', + 'Test content', + )->andReturn(true); + + $factory = Mockery::mock(\Illuminate\View\Factory::class); + $factory->shouldReceive('getFinder->find')->andReturn('Test content'); + app()->instance('view', $factory); + + PaymentGateways::createPartials(); +}); diff --git a/tests/Concerns/WithApplicableFeeTest.php b/tests/Concerns/WithApplicableFeeTest.php index 3618815..f0194ec 100644 --- a/tests/Concerns/WithApplicableFeeTest.php +++ b/tests/Concerns/WithApplicableFeeTest.php @@ -27,6 +27,11 @@ public function defineFieldsConfig() return __DIR__.'/../_fixtures/fields'; } + public function testValidatePaymentMethod($order, $host) + { + $this->validatePaymentMethod($order, $host); + } + public function validatesApplicable($order): void { $this->validateApplicableFee($order, $this->model); @@ -34,6 +39,12 @@ public function validatesApplicable($order): void }; }); +it('validates payment method successfully', function() { + $this->trait->testValidatePaymentMethod($this->order, $this->payment); + + expect(true)->toBeTrue(); +}); + it('validates applicable fee successfully', function() { $this->trait->validatesApplicable($this->order); diff --git a/tests/Concerns/WithPaymentProfileTest.php b/tests/Concerns/WithPaymentProfileTest.php index 0424f71..43d5ed8 100644 --- a/tests/Concerns/WithPaymentProfileTest.php +++ b/tests/Concerns/WithPaymentProfileTest.php @@ -26,21 +26,33 @@ public function defineFieldsConfig() }; }); -it('throws exception if updatePaymentProfile is not implemented', function() { +it('returns false when supportsPaymentProfiles is not implemented', function() { + $result = $this->trait->supportsPaymentProfiles(); + + expect($result)->toBeFalse(); +}); + +it('returns null when paymentProfileExists is not implemented', function() { + $result = $this->trait->paymentProfileExists($this->customer); + + expect($result)->toBeNull(); +}); + +it('throws exception when updatePaymentProfile is not implemented', function() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Method updatePaymentProfile must be implemented on your custom payment class.'); $this->trait->updatePaymentProfile($this->customer, []); }); -it('throws exception if deletePaymentProfile is not implemented', function() { +it('throws exception when deletePaymentProfile is not implemented', function() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Method deletePaymentProfile must be implemented on your custom payment class.'); $this->trait->deletePaymentProfile($this->customer, $this->profile); }); -it('throws exception if payFromPaymentProfile is not implemented', function() { +it('throws exception when payFromPaymentProfile is not implemented', function() { $this->expectException(\LogicException::class); $this->expectExceptionMessage('Method payFromPaymentProfile must be implemented on your custom payment class.'); diff --git a/tests/ExtensionTest.php b/tests/ExtensionTest.php index 51fc2f7..3f504ff 100644 --- a/tests/ExtensionTest.php +++ b/tests/ExtensionTest.php @@ -125,7 +125,10 @@ public function observers(): array }); it('syncs payments on theme activation', function() { - Event::shouldReceive('listen')->with('main.theme.activated', Mockery::any())->once(); + Event::shouldReceive('listen')->with('main.theme.activated', Mockery::on(function($callback) { + $callback(); + return true; + }))->once(); $this->extension->boot(); }); diff --git a/tests/Models/Observers/PaymentObserverTest.php b/tests/Models/Observers/PaymentObserverTest.php index 5978e5d..420dcf1 100644 --- a/tests/Models/Observers/PaymentObserverTest.php +++ b/tests/Models/Observers/PaymentObserverTest.php @@ -15,7 +15,7 @@ $observer = new PaymentObserver(); $observer->retrieved($payment); -})->only(); +}); it('purges config fields on saving if payment exists', function() { $payment = Mockery::mock(Payment::class)->makePartial(); diff --git a/tests/Models/Observers/PaymentProfileObserverTest.php b/tests/Models/Observers/PaymentProfileObserverTest.php new file mode 100644 index 0000000..054c95e --- /dev/null +++ b/tests/Models/Observers/PaymentProfileObserverTest.php @@ -0,0 +1,18 @@ +create(['is_primary' => false]); + + $paymentProfile->is_primary = true; + $paymentProfile->save(); + + $observer = new PaymentProfileObserver(); + $observer->saved($paymentProfile); + + expect($paymentProfile->is_primary)->toBeTrue(); +}); diff --git a/tests/Models/PaymentTest.php b/tests/Models/PaymentTest.php index 9fd51de..289c8b2 100644 --- a/tests/Models/PaymentTest.php +++ b/tests/Models/PaymentTest.php @@ -138,12 +138,12 @@ }); it('returns false if payment profile does not exist for customer', function() { - $this->payment->shouldReceive('getGatewayObject->paymentProfileExists')->with($this->customer)->andReturn(false); - $this->payment->shouldReceive('findPaymentProfile')->with($this->customer)->andReturn(null); + $this->payment->shouldReceive('getGatewayObject->paymentProfileExists')->with($this->customer)->andReturn(null); + $this->payment->shouldReceive('findPaymentProfile')->with($this->customer)->andReturn(true); $result = $this->payment->paymentProfileExists($this->customer); - expect($result)->toBeFalse(); + expect($result)->toBeTrue(); }); it('deletes payment profile for customer', function() { diff --git a/tests/Payments/AuthorizeNetAimTest.php b/tests/Payments/AuthorizeNetAimTest.php index 62360ec..2cccf74 100644 --- a/tests/Payments/AuthorizeNetAimTest.php +++ b/tests/Payments/AuthorizeNetAimTest.php @@ -6,13 +6,13 @@ use Igniter\Cart\Models\Order; use Igniter\Flame\Exception\ApplicationException; use Igniter\Main\Classes\MainController; +use Igniter\PayRegister\Classes\AuthorizeNetClient; +use Igniter\PayRegister\Classes\AuthorizeNetTransactionRequest; use Igniter\PayRegister\Models\Payment; use Igniter\PayRegister\Models\PaymentLog; use Igniter\PayRegister\Payments\AuthorizeNetAim; use Illuminate\Support\Facades\Event; use Mockery; -use net\authorize\api\contract\v1\CreateTransactionRequest; -use net\authorize\api\contract\v1\MerchantAuthenticationType; use net\authorize\api\contract\v1\TransactionResponseType; use net\authorize\api\contract\v1\TransactionResponseType\MessagesAType\MessageAType; @@ -31,6 +31,29 @@ expect($this->authorizeNetAim->defineFieldsConfig())->toBe('igniter.payregister::/models/authorizenetaim'); }); +it('returns hidden fields with default values', function() { + $gateway = new AuthorizeNetAim(); + + $hiddenFields = $gateway->getHiddenFields(); + + expect($hiddenFields)->toBeArray() + ->and($hiddenFields)->toHaveKey('authorizenetaim_DataValue', '') + ->and($hiddenFields)->toHaveKey('authorizenetaim_DataDescriptor', ''); +}); + +it('returns accepted cards with correct labels', function() { + $gateway = new AuthorizeNetAim(); + + $acceptedCards = $gateway->getAcceptedCards(); + + expect($acceptedCards)->toBeArray() + ->and($acceptedCards)->toHaveKey('visa', 'lang:igniter.payregister::default.authorize_net_aim.text_visa') + ->and($acceptedCards)->toHaveKey('mastercard', 'lang:igniter.payregister::default.authorize_net_aim.text_mastercard') + ->and($acceptedCards)->toHaveKey('american_express', 'lang:igniter.payregister::default.authorize_net_aim.text_american_express') + ->and($acceptedCards)->toHaveKey('jcb', 'lang:igniter.payregister::default.authorize_net_aim.text_jcb') + ->and($acceptedCards)->toHaveKey('diners_club', 'lang:igniter.payregister::default.authorize_net_aim.text_diners_club'); +}); + it('returns correct endpoint for authorizenet test mode', function() { $this->payment->transaction_mode = 'test'; @@ -62,7 +85,7 @@ ]); it('adds JavaScript file to the controller', function() { - $controller = Mockery::mock(MainController::class); + $controller = mock(MainController::class); $controller ->shouldReceive('addJs') @@ -73,22 +96,22 @@ }); it('processes authorizenet payment form and logs successful payment', function() { + $this->payment->transaction_type = 'auth'; $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); - $response = Mockery::mock(TransactionResponseType::class); + $request = mock(AuthorizeNetTransactionRequest::class); + $response = mock(TransactionResponseType::class); $response->shouldReceive('getResponseCode')->andReturn('1')->twice(); $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); $response->shouldReceive('getTransId')->andReturn('12345')->once(); $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createAcceptPayment')->andReturn($response)->once(); - $authorizeNetAim->shouldReceive('shouldAuthorizePayment')->andReturnFalse()->once(); - - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->payment_method = $this->payment; $order->order_total = 100; $order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, Mockery::any(), Mockery::any(), true)->once(); @@ -98,45 +121,72 @@ $this->payment->applyGatewayClass(); - $authorizeNetAim->processPaymentForm($data, $this->payment, $order); + $this->authorizeNetAim->processPaymentForm($data, $this->payment, $order); +}); + +it('processes authorizenet payment form and logs authorized payment', function() { + $this->payment->transaction_type = 'auth_only'; + $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); + $request = mock(AuthorizeNetTransactionRequest::class); + $response = mock(TransactionResponseType::class); + $response->shouldReceive('getResponseCode')->andReturn('1'); + $response->shouldReceive('getMessages')->andReturn([$messageAType]); + $response->shouldReceive('getTransId')->andReturn('12345'); + $response->shouldReceive('getAccountNumber')->andReturn('****1111'); + $response->shouldReceive('getAccountType')->andReturn('Visa'); + $response->shouldReceive('getAuthCode')->andReturn('auth123'); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + + $order = mock(Order::class)->makePartial(); + $order->payment_method = $this->payment; + $order->order_total = 100; + $order->shouldReceive('logPaymentAttempt')->with('Payment authorized', 1, Mockery::any(), Mockery::any())->once(); + $order->shouldReceive('updateOrderStatus'); + $order->shouldReceive('markAsPaymentProcessed'); + $data = ['authorizenetaim_DataDescriptor' => 'descriptor', 'authorizenetaim_DataValue' => 'value']; + + $this->payment->applyGatewayClass(); + + $this->authorizeNetAim->processPaymentForm($data, $this->payment, $order); }); it('throws exception if authorizenet payment form processing fails', function() { - $order = Mockery::mock(Order::class)->makePartial(); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andThrow(new Exception('Payment error')); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + $order = mock(Order::class)->makePartial(); $order->shouldReceive('logPaymentAttempt')->with('Payment error -> Payment error', 0, Mockery::any())->once(); $order->payment_method = $this->payment; $order->order_total = 100; $data = ['authorizenetaim_DataDescriptor' => 'descriptor', 'authorizenetaim_DataValue' => 'value']; - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createAcceptPayment')->andThrow(new Exception('Payment error'))->once(); - $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); $this->payment->applyGatewayClass(); - $authorizeNetAim->processPaymentForm($data, $this->payment, $order); + $this->authorizeNetAim->processPaymentForm($data, $this->payment, $order); }); it('processes authorizenet payment form and logs failed payment', function() { $messageAType = (new MessageAType())->setCode('2')->setDescription('Declined'); - $response = Mockery::mock(TransactionResponseType::class); + $request = mock(AuthorizeNetTransactionRequest::class); + $response = mock(TransactionResponseType::class); $response->shouldReceive('getResponseCode')->andReturn('2')->twice(); $response->shouldReceive('getMessages')->andReturn([$messageAType])->atMost(5); $response->shouldReceive('getTransId')->andReturn('12345')->once(); $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createAcceptPayment')->andReturn($response)->once(); - - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->order_id = 123; $order->payment_method = $this->payment; $order->order_total = 100; @@ -147,120 +197,157 @@ $this->payment->applyGatewayClass(); - $authorizeNetAim->processPaymentForm($data, $this->payment, $order); + $this->authorizeNetAim->processPaymentForm($data, $this->payment, $order); }); it('processes authorizenet full refund successfully', function() { $messageAType = (new MessageAType())->setCode('2')->setDescription('Declined'); - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); + $paymentLog = mock(PaymentLog::class)->makePartial(); $paymentLog->shouldReceive('markAsRefundProcessed')->once(); $paymentLog->refunded_at = null; $paymentLog->response = ['status' => '1', 'id' => '12345', 'card_holder' => '****1111']; - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->shouldReceive('logPaymentAttempt'); $order->order_total = 100; - $response = Mockery::mock(TransactionResponseType::class); + $response = mock(TransactionResponseType::class); $response->shouldReceive('getResponseCode')->andReturn('1')->once(); $response->shouldReceive('getTransId')->andReturn('54321')->once(); $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); $data = ['refund_type' => 'full']; - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createRefundPayment')->andReturn($response)->once(); - - $authorizeNetAim->processRefundForm($data, $order, $paymentLog); + $this->authorizeNetAim->processRefundForm($data, $order, $paymentLog); }); it('authorizenet: throws exception if refund amount exceeds order total', function() { - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); + $paymentLog = mock(PaymentLog::class)->makePartial(); $paymentLog->refunded_at = null; $paymentLog->response = ['status' => '1', 'id' => '12345', 'card_holder' => '****1111']; - - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->order_total = 100; - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $data = ['refund_type' => 'partial', 'refund_amount' => 150]; $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Refund amount should be be less than or equal to the order total'); - $authorizeNetAim->processRefundForm($data, $order, $paymentLog); + $this->authorizeNetAim->processRefundForm($data, $order, $paymentLog); +}); + +it('authorizenet: throws exception when refund request fails', function() { + $paymentLog = mock(PaymentLog::class)->makePartial(); + $paymentLog->refunded_at = null; + $paymentLog->response = ['status' => '1', 'id' => '12345', 'card_holder' => '****1111']; + $order = mock(Order::class)->makePartial(); + $order->order_total = 100; + $order->shouldReceive('logPaymentAttempt')->with('Refund failed -> Refund request error', 0, Mockery::any(), [])->once(); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andThrow(new Exception('Refund request error')); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + + $data = ['refund_type' => 'partial', 'refund_amount' => 100]; + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Refund failed, please try again later or contact system administrator'); + + $this->authorizeNetAim->processRefundForm($data, $order, $paymentLog); }); it('authorizenet: captures authorized payment successfully', function() { Event::fake(); + $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); + $response = mock(TransactionResponseType::class); + $response->shouldReceive('getResponseCode')->andReturn('1')->twice(); + $response->shouldReceive('getTransId')->andReturn('54321')->once(); + $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); + $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); + $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); + $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $request = mock(AuthorizeNetTransactionRequest::class); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); + $paymentLog = mock(PaymentLog::class)->makePartial(); $paymentLog->response = ['id' => '12345']; - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->hash = 'order_hash'; - $order->shouldReceive('logPaymentAttempt')->once(); + $order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, [], Mockery::any(), true)->once(); $order->shouldReceive('payment_logs->firstWhere') ->with('is_success', true) ->andReturn($paymentLog) ->once(); - $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); - $response = Mockery::mock(TransactionResponseType::class); - $response->shouldReceive('getResponseCode')->andReturn('1')->twice(); - $response->shouldReceive('getTransId')->andReturn('54321')->once(); - $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); - $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); - $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); - $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $expectedResponse = $this->authorizeNetAim->captureAuthorizedPayment($order); - $request = new CreateTransactionRequest; - $request->setMerchantAuthentication(new MerchantAuthenticationType); + Event::assertDispatched('payregister.authorizenetaim.extendCaptureRequest'); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createClient->createTransactionRequest')->andReturn($request)->once(); - $authorizeNetAim->shouldReceive('createClient->createTransaction')->andReturn($response)->once(); + expect($response)->toEqual($expectedResponse); +}); - $authorizeNetAim->captureAuthorizedPayment($order); +it('authorizenet: captures authorized payment failed', function() { + Event::fake(); + $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); + $response = mock(TransactionResponseType::class); + $response->shouldReceive('getResponseCode')->andReturn('2')->twice(); + $response->shouldReceive('getTransId')->andReturn('54321'); + $response->shouldReceive('getMessages')->andReturn([$messageAType]); + $response->shouldReceive('getAccountNumber')->andReturn('****1111'); + $response->shouldReceive('getAccountType')->andReturn('Visa'); + $response->shouldReceive('getAuthCode')->andReturn('auth123'); + $request = mock(AuthorizeNetTransactionRequest::class); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + + $paymentLog = mock(PaymentLog::class)->makePartial(); + $paymentLog->response = ['id' => '12345']; + + $order = mock(Order::class)->makePartial(); + $order->hash = 'order_hash'; + $order->shouldReceive('logPaymentAttempt')->with('Payment failed', 0, [], Mockery::any())->once(); + $order->shouldReceive('payment_logs->firstWhere') + ->with('is_success', true) + ->andReturn($paymentLog) + ->once(); + + $expectedResponse = $this->authorizeNetAim->captureAuthorizedPayment($order); Event::assertDispatched('payregister.authorizenetaim.extendCaptureRequest'); + + expect($response)->toEqual($expectedResponse); }); it('authorizenet: throws exception if no successful transaction to capture', function() { - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->shouldReceive('payment_logs->firstWhere') ->with('is_success', true) ->andReturnNull() ->once(); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No successful transaction to capture'); - $authorizeNetAim->captureAuthorizedPayment($order); + $this->authorizeNetAim->captureAuthorizedPayment($order); }); it('authorizenet: cancels authorized payment successfully', function() { Event::fake(); - - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); + $paymentLog = mock(PaymentLog::class)->makePartial(); $paymentLog->is_success = true; $paymentLog->response = ['id' => '12345']; - - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->hash = 'order_hash'; $order->shouldReceive('logPaymentAttempt'); $order->shouldReceive('payment_logs->firstWhere') @@ -269,42 +356,70 @@ ->once(); $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); - $response = Mockery::mock(TransactionResponseType::class); + $response = mock(TransactionResponseType::class); $response->shouldReceive('getResponseCode')->andReturn('1')->twice(); $response->shouldReceive('getTransId')->andReturn('54321')->once(); $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $request = mock(AuthorizeNetTransactionRequest::class); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + + $expectedResponse = $this->authorizeNetAim->cancelAuthorizedPayment($order); - $request = new CreateTransactionRequest; - $request->setMerchantAuthentication(new MerchantAuthenticationType); + expect($response)->toEqual($expectedResponse); + Event::assertDispatched('payregister.authorizenetaim.extendCancelRequest'); +}); + +it('authorizenet: cancels authorized payment failed', function() { + Event::fake(); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $authorizeNetAim->shouldReceive('createClient->createTransactionRequest')->andReturn($request)->once(); - $authorizeNetAim->shouldReceive('createClient->createTransaction')->andReturn($response)->once(); + $paymentLog = mock(PaymentLog::class)->makePartial(); + $paymentLog->is_success = true; + $paymentLog->response = ['id' => '12345']; - $authorizeNetAim->cancelAuthorizedPayment($order); + $order = mock(Order::class)->makePartial(); + $order->hash = 'order_hash'; + $order->shouldReceive('logPaymentAttempt')->with('Canceling payment failed', 0, [], Mockery::any())->once(); + $order->shouldReceive('payment_logs->firstWhere') + ->with('is_success', true) + ->andReturn($paymentLog) + ->once(); + $messageAType = (new MessageAType())->setCode('1')->setDescription('Success'); + $response = mock(TransactionResponseType::class); + $response->shouldReceive('getResponseCode')->andReturn('2')->twice(); + $response->shouldReceive('getTransId')->andReturn('54321')->once(); + $response->shouldReceive('getMessages')->andReturn([$messageAType])->twice(); + $response->shouldReceive('getAccountNumber')->andReturn('****1111')->once(); + $response->shouldReceive('getAccountType')->andReturn('Visa')->once(); + $response->shouldReceive('getAuthCode')->andReturn('auth123')->once(); + $request = mock(AuthorizeNetTransactionRequest::class); + $authorizeClient = mock(AuthorizeNetClient::class)->makePartial(); + $authorizeClient->shouldReceive('createTransactionRequest')->andReturn($request); + $authorizeClient->shouldReceive('createTransaction')->andReturn($response); + app()->instance(AuthorizeNetClient::class, $authorizeClient); + + $expectedResponse = $this->authorizeNetAim->cancelAuthorizedPayment($order); + + expect($response)->toEqual($expectedResponse); Event::assertDispatched('payregister.authorizenetaim.extendCancelRequest'); }); it('authorizenet: throws exception if no successful transaction to cancel', function() { - $order = Mockery::mock(Order::class)->makePartial(); + $order = mock(Order::class)->makePartial(); $order->shouldReceive('payment_logs->firstWhere') ->with('is_success', true) ->andReturnNull() ->once(); - $authorizeNetAim = Mockery::mock(AuthorizeNetAim::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No successful transaction to capture'); - $authorizeNetAim->cancelAuthorizedPayment($order); + $this->authorizeNetAim->cancelAuthorizedPayment($order); }); diff --git a/tests/Payments/MollieTest.php b/tests/Payments/MollieTest.php index 8630229..0ea586b 100644 --- a/tests/Payments/MollieTest.php +++ b/tests/Payments/MollieTest.php @@ -7,8 +7,11 @@ use Igniter\PayRegister\Models\PaymentProfile; use Igniter\PayRegister\Payments\Mollie; use Igniter\User\Models\Customer; +use Illuminate\Support\Facades\Mail; +use Mollie\Api\Endpoints\CustomerEndpoint; use Mollie\Api\Endpoints\PaymentEndpoint; use Mollie\Api\MollieApiClient; +use Mollie\Api\Resources\Customer as MollieCustomer; use Mollie\Api\Resources\Payment as MolliePayment; beforeEach(function() { @@ -16,8 +19,6 @@ 'class_name' => Mollie::class, ]); $this->paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $this->order = Mockery::mock(Order::class)->makePartial(); - $this->order->payment_method = $this->payment; $this->mollie = new Mollie($this->payment); }); @@ -45,72 +46,140 @@ it('returns false if not in mollie test mode', function() { $this->payment->transaction_mode = 'live'; + expect($this->mollie->isTestMode())->toBeFalse(); }); it('returns mollie test API key in test mode', function() { $this->payment->transaction_mode = 'test'; $this->payment->test_api_key = 'test_key'; + expect($this->mollie->getApiKey())->toBe('test_key'); }); it('returns mollie live API key in live mode', function() { $this->payment->transaction_mode = 'live'; $this->payment->live_api_key = 'live_key'; + expect($this->mollie->getApiKey())->toBe('live_key'); }); it('processes mollie payment form and redirects to checkout url', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; - - $paymentProfile = Mockery::mock(PaymentProfile::class)->makePartial(); - $paymentProfile->profile_data = ['card_id' => '123', 'customer_id' => '456']; + Mail::fake(); + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer_id, + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => '123', 'customer_id' => '456'], + ]); $molliePayment = Mockery::mock(MolliePayment::class); $molliePayment->shouldReceive('isOpen')->andReturn(true)->once(); $molliePayment->shouldReceive('getCheckoutUrl')->andReturn('http://checkout.url')->once(); + $customerEndpoint = Mockery::mock(CustomerEndpoint::class); + $customerEndpoint->shouldReceive('get')->andReturnNull(); $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); $paymentEndpoint->shouldReceive('create')->andReturn($molliePayment)->once(); + $customerEndpoint->shouldReceive('create')->andReturn(mock(MollieCustomer::class))->once(); $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); - $mollieClient->setApiKey('test_'.str_random(30)); $mollieClient->payments = $paymentEndpoint; + $mollieClient->customers = $customerEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('validateApplicableFee')->once(); - $mollie->shouldReceive('updatePaymentProfile')->andReturn($paymentProfile)->once(); - $mollie->shouldReceive('createClient')->andReturn($mollieClient)->once(); + $response = $this->mollie->processPaymentForm([], $this->payment, $order); - $response = $mollie->processPaymentForm([], $this->payment, $this->order); + expect($response->getTargetUrl())->toBe('http://checkout.url'); +}); + +it('throws exception when fails to create payment profile', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer_id, + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => '123', 'customer_id' => '456'], + ]); + + $customerEndpoint = Mockery::mock(CustomerEndpoint::class); + $customerEndpoint->shouldReceive('get')->andReturnNull(); + $customerEndpoint->shouldReceive('create')->andReturnNull(); + + $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); + $mollieClient->customers = $customerEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Unable to create customer'); + + $response = $this->mollie->processPaymentForm([], $this->payment, $order); expect($response->getTargetUrl())->toBe('http://checkout.url'); }); it('throws exception if mollie payment creation fails', function() { - $this->order->shouldReceive('logPaymentAttempt')->with('Payment error -> Payment error', 0, Mockery::any(), Mockery::any())->once(); + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $payment = Mockery::mock(MolliePayment::class); $payment->shouldReceive('isOpen')->andReturn(false)->once(); $payment->shouldReceive('getMessage')->andReturn('Payment error')->once(); + $customerEndpoint = Mockery::mock(CustomerEndpoint::class); + $customerEndpoint->shouldReceive('create')->andReturn((object)['id' => 'customer_id'])->once(); $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); $paymentEndpoint->shouldReceive('create')->andReturn($payment)->once(); - $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); - $mollieClient->setApiKey('test_'.str_random(30)); $mollieClient->payments = $paymentEndpoint; + $mollieClient->customers = $customerEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('createClient')->andReturn($mollieClient); - $mollie->shouldReceive('validateApplicableFee')->once(); + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); + + $this->mollie->processPaymentForm([], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + ]); +}); + +it('throws exception if mollie payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); + $paymentEndpoint->shouldReceive('create')->andThrow(new Exception('Payment error'))->once(); + $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); + $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); - $mollie->processPaymentForm([], $this->payment, $this->order); + $this->mollie->processPaymentForm([], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + ]); }); it('processes mollie return url and updates order status', function() { @@ -119,33 +188,34 @@ 'cancel' => 'http://cancel.url', ]); + Mail::fake(); + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); $this->payment->applyGatewayClass(); - $this->order->hash = 'order_hash'; - $this->order->order_id = 1; - $this->order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, Mockery::any(), Mockery::any(), true)->once(); - $this->order->shouldReceive('updateOrderStatus')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $molliePayment = Mockery::mock(MolliePayment::class); $molliePayment->shouldReceive('isPaid')->andReturn(true)->once(); - $molliePayment->metadata = ['order_id' => 1]; + $molliePayment->metadata = ['order_id' => $order->getKey()]; $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); $paymentEndpoint->shouldReceive('get')->andReturn($molliePayment)->once(); - $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); - $mollieClient->setApiKey('test_'.str_random(30)); $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('createClient')->andReturn($mollieClient); - $mollie->shouldReceive('createOrderModel->whereHash->first')->andReturn($this->order); - - $response = $mollie->processReturnUrl(['order_hash']); + $response = $this->mollie->processReturnUrl([$order->hash]); expect($response->getTargetUrl())->toContain('http://redirect.url'); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); }); it('throws exception if no order found in mollie return url', function() { @@ -165,13 +235,13 @@ 'id' => 'payment_id', ]); + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); $this->payment->applyGatewayClass(); - $this->order->hash = 'order_hash'; - $this->order->order_id = 1; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->order->shouldReceive('updateOrderStatus')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $molliePayment = Mockery::mock(MolliePayment::class); $molliePayment->shouldReceive('isPaid')->andReturn(true)->once(); @@ -180,35 +250,69 @@ $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); $mollieClient->setApiKey('test_'.str_random(30)); $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); + + $response = $this->mollie->processNotifyUrl([$order->hash]); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('createClient')->andReturn($mollieClient); - $mollie->shouldReceive('createOrderModel->whereHash->first')->andReturn($this->order); + expect($response->getData())->success->toBe(true); - $response = $mollie->processNotifyUrl(['order_hash']); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('processes mollie notify url fails and updates order status', function() { + request()->merge([ + 'id' => 'payment_id', + ]); + + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $this->payment->applyGatewayClass(); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + $molliePayment = Mockery::mock(MolliePayment::class); + $molliePayment->shouldReceive('isPaid')->andReturn(false)->once(); + $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); + $paymentEndpoint->shouldReceive('get')->andReturn($molliePayment)->once(); + $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); + $mollieClient->setApiKey('test_'.str_random(30)); + $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); + + $response = $this->mollie->processNotifyUrl([$order->hash]); expect($response->getData())->success->toBe(true); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment unsuccessful', + 'is_success' => 0, + ]); }); it('throws exception if no order found in mollie notify url', function() { $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No order found'); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('createOrderModel->whereHash->first')->andReturnNull(); - $this->mollie->processNotifyUrl(['invalid_hash']); }); it('processes mollie refund form and logs refund attempt', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); $this->paymentLog->refunded_at = null; $this->paymentLog->response = ['status' => 'paid', 'id' => 'payment_id']; - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->once(); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $this->paymentLog->shouldReceive('markAsRefundProcessed')->once(); $molliePayment = Mockery::mock(MolliePayment::class); @@ -220,23 +324,59 @@ $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); $paymentEndpoint->shouldReceive('get')->andReturn($molliePayment)->once(); $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); - $mollieClient->setApiKey('test_'.str_random(30)); $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); + + $this->mollie->processRefundForm(['refund_type' => 'full'], $order, $this->paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => sprintf('Payment %s refund processed -> (%s: %s)', 'payment_id', 'full', 'refund_id'), + 'is_success' => 1, + ]); +}); + +it('processes mollie refund request fails and logs refund attempt', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_api_key = 'test_'.str_random(30); + $this->paymentLog->refunded_at = null; + $this->paymentLog->response = ['status' => 'paid', 'id' => 'payment_id']; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + $paymentEndpoint = Mockery::mock(PaymentEndpoint::class); + $paymentEndpoint->shouldReceive('get')->andThrow(new Exception('Refund Error'))->once(); + $mollieClient = Mockery::mock(MollieApiClient::class)->makePartial(); + $mollieClient->payments = $paymentEndpoint; + app()->instance(MollieApiClient::class, $mollieClient); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Refund failed'); - $mollie = Mockery::mock(Mollie::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $mollie->shouldReceive('createClient')->andReturn($mollieClient); + $this->mollie->bindEvent('mollie.extendRefundFields', function($fields, $order, $data) { + return [ + 'extra_field' => 'extra_value', + ]; + }); - $mollie->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->mollie->processRefundForm(['refund_type' => 'full'], $order, $this->paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Refund failed -> Refund Error', + 'is_success' => 1, + ]); }); it('throws exception if no mollie charge to refund', function() { + $order = Order::factory()->create(['order_total' => 100]); $this->paymentLog->refunded_at = null; $this->paymentLog->response = ['status' => 'not_paid']; $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No charge to refund'); - $this->mollie->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->mollie->processRefundForm(['refund_type' => 'full'], $order, $this->paymentLog); }); diff --git a/tests/Payments/PaypalExpressTest.php b/tests/Payments/PaypalExpressTest.php index 72e3677..6495e45 100644 --- a/tests/Payments/PaypalExpressTest.php +++ b/tests/Payments/PaypalExpressTest.php @@ -5,6 +5,7 @@ use Exception; use Igniter\Cart\Models\Order; use Igniter\Flame\Exception\ApplicationException; +use Igniter\PayRegister\Classes\PayPalClient; use Igniter\PayRegister\Models\Payment; use Igniter\PayRegister\Models\PaymentLog; use Igniter\PayRegister\Payments\PaypalExpress; @@ -16,9 +17,6 @@ $this->payment = Payment::factory()->create([ 'class_name' => PaypalExpress::class, ]); - $this->paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $this->order = Mockery::mock(Order::class)->makePartial(); - $this->order->payment_method = $this->payment; $this->paypalExpress = new PaypalExpress($this->payment); }); @@ -91,54 +89,80 @@ expect($this->paypalExpress->getTransactionMode())->toBe('CAPTURE'); }); -it('processes paypal express payment form and redirects to payer action url', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; +it('processes payment form and redirects to payer action url', function() { + $this->payment->api_action = 'authorization'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $response = Mockery::mock(Response::class); $response->shouldReceive('ok')->andReturn(true); $response->shouldReceive('json')->with('links', [])->andReturn([['rel' => 'payer-action', 'href' => 'http://payer.action.url']]); - $paypalExpress = Mockery::mock(PaypalExpress::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $paypalExpress->shouldReceive('getTransactionMode')->andReturn('AUTHORIZE')->once(); - $paypalExpress->shouldReceive('createClient->createOrder')->andReturn($response); + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('createOrder')->andReturn($response); + app()->instance(PayPalClient::class, $paypalClient); - $result = $paypalExpress->processPaymentForm([], $this->payment, $this->order); + $result = $this->paypalExpress->processPaymentForm([], $this->payment, $order); expect($result->getTargetUrl())->toBe('http://payer.action.url'); }); -it('throws exception if paypal express payment creation fails', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt') - ->with('Payment error -> Payment error', 0, Mockery::any(), Mockery::any()) - ->once(); +it('throws exception when payment response is not successful', function() { + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); - $paypalExpress = Mockery::mock(PaypalExpress::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $paypalExpress->shouldReceive('validateApplicableFee')->once(); - $paypalExpress->shouldReceive('getTransactionMode')->andReturn('AUTHORIZE')->once(); - $paypalExpress->shouldReceive('createClient->createOrder')->andThrow(new Exception('Payment error')); + $response = Mockery::mock(Response::class); + $response->shouldReceive('ok')->andReturn(false); + $response->shouldReceive('json')->withNoArgs()->andReturn([])->once(); + $response->shouldReceive('json')->with('message')->andReturn('Payment error')->once(); + $response->shouldReceive('json')->with('links', [])->andReturn([['rel' => 'payer-action', 'href' => 'http://payer.action.url']]); + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('createOrder')->andReturn($response); + app()->instance(PayPalClient::class, $paypalClient); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); - $paypalExpress->processPaymentForm([], $this->payment, $this->order); + $this->paypalExpress->processPaymentForm([], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + ]); +}); + +it('throws exception when payment request fails', function() { + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('createOrder')->andThrow(new Exception('Payment error')); + app()->instance(PayPalClient::class, $paypalClient); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); + + $this->paypalExpress->processPaymentForm([], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + ]); }); it('processes paypal express return url and updates order status', function(string $transactionMode) { request()->merge(['token' => 'test_token']); $this->payment->applyGatewayClass(); - $this->order->hash = 'order_hash'; - $this->order->order_id = 1; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->order->shouldReceive('updateOrderStatus')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $response = Mockery::mock(Response::class); $response->shouldReceive('json')->withNoArgs()->andReturn([]); @@ -148,15 +172,13 @@ $response->shouldReceive('json')->with('purchase_units.0.payments.authorizations.0.status')->andReturn('CREATED'); } - $paypalExpress = Mockery::mock(PaypalExpress::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $paypalExpress->shouldReceive('createClient->getOrder')->andReturn(['status' => 'APPROVED', 'intent' => $transactionMode]); - $paypalExpress->shouldReceive('createClient->captureOrder')->andReturn($response); - $paypalExpress->shouldReceive('createClient->authorizeOrder')->andReturn($response); - $paypalExpress->shouldReceive('createOrderModel->whereHash->first')->andReturn($this->order); + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('getOrder')->andReturn(['status' => 'APPROVED', 'intent' => $transactionMode]); + $paypalClient->shouldReceive('captureOrder')->andReturn($response); + $paypalClient->shouldReceive('authorizeOrder')->andReturn($response); + app()->instance(PayPalClient::class, $paypalClient); - $result = $paypalExpress->processReturnUrl(['order_hash']); + $result = $this->paypalExpress->processReturnUrl([$order->hash]); expect($result->getTargetUrl())->toContain('http://localhost/checkout'); })->with([ @@ -164,7 +186,7 @@ ['AUTHORIZE'], ]); -it('throws exception if no order found in paypal express return url', function() { +it('throws exception when no order found in paypal express return url', function() { request()->merge([ 'redirect' => 'http://redirect.url', 'cancel' => 'http://cancel.url', @@ -177,18 +199,19 @@ }); it('processes paypal express cancel url and logs payment attempt', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $this->payment->applyGatewayClass(); - $this->order->hash = 'order_hash'; - $this->order->shouldReceive('logPaymentAttempt')->once(); - - $paypalExpress = Mockery::mock(PaypalExpress::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $paypalExpress->shouldReceive('createOrderModel->whereHash->first')->andReturn($this->order); - $result = $paypalExpress->processCancelUrl(['order_hash']); + $result = $this->paypalExpress->processCancelUrl([$order->hash]); expect($result->getTargetUrl())->toContain('http://localhost/checkout'); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment canceled by customer', + ]); }); it('throws exception if no order found in paypal express cancel url', function() { @@ -199,29 +222,70 @@ }); it('processes paypal express refund form and logs refund attempt', function() { - $this->paymentLog->refunded_at = null; - $this->paymentLog->response = ['purchase_units' => [['payments' => ['captures' => [['status' => 'COMPLETED', 'id' => 'payment_id']]]]]]; - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->paymentLog->shouldReceive('markAsRefundProcessed')->once(); + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['purchase_units' => [['payments' => ['captures' => [['id' => 'payment_id', 'status' => 'COMPLETED']]]]]], + ]); $response = Mockery::mock(Response::class); $response->shouldReceive('json')->with('id')->andReturn('refund_id'); $response->shouldReceive('json')->withNoArgs()->andReturn([]); - $paypalExpress = Mockery::mock(PaypalExpress::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $paypalExpress->shouldReceive('createClient->refundPayment')->andReturn($response); + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('refundPayment')->andReturn($response); + app()->instance(PayPalClient::class, $paypalClient); + + $this->paypalExpress->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); - $paypalExpress->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => sprintf('Payment %s refund processed -> (%s: %s)', 'payment_id', 'full', 'refund_id'), + 'is_success' => 1, + ]); +}); + +it('throws exception when refund payment request fails', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['purchase_units' => [['payments' => ['captures' => [['status' => 'COMPLETED']]]]]], + ]); + + $paypalClient = Mockery::mock(PayPalClient::class)->makePartial(); + $paypalClient->shouldReceive('refundPayment')->andThrow(new Exception('Refund Error')); + app()->instance(PayPalClient::class, $paypalClient); + $this->paypalExpress->bindEvent('paypalexpress.extendRefundFields', function($fields, $order, $data) { + return [ + 'extra_field' => 'extra_value', + ]; + }); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Refund failed'); + + $this->paypalExpress->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Refund failed -> Refund Error', + 'is_success' => 1, + ]); }); it('throws exception if no paypal express charge to refund', function() { - $this->paymentLog->refunded_at = null; - $this->paymentLog->response = ['purchase_units' => [['payments' => ['captures' => [['status' => 'not_completed']]]]]]; + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'response' => ['purchase_units' => [['payments' => ['captures' => [['status' => 'not_completed']]]]]], + ]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No charge to refund'); - $this->paypalExpress->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->paypalExpress->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); }); diff --git a/tests/Payments/SquareTest.php b/tests/Payments/SquareTest.php index 28f0cb4..d9ed320 100644 --- a/tests/Payments/SquareTest.php +++ b/tests/Payments/SquareTest.php @@ -8,25 +8,44 @@ use Igniter\PayRegister\Models\Payment; use Igniter\PayRegister\Models\PaymentLog; use Igniter\PayRegister\Models\PaymentProfile; -use Igniter\PayRegister\Payments\PaypalExpress; use Igniter\PayRegister\Payments\Square; use Igniter\User\Models\Customer; use Mockery; +use Square\Apis\CardsApi; +use Square\Apis\CustomersApi; +use Square\Apis\PaymentsApi; use Square\Apis\RefundsApi; use Square\Http\ApiResponse; use Square\Models\Error; use Square\SquareClient; +use Square\SquareClientBuilder; beforeEach(function() { $this->payment = Payment::factory()->create([ - 'class_name' => PaypalExpress::class, + 'class_name' => Square::class, ]); - $this->paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $this->order = Mockery::mock(Order::class)->makePartial(); - $this->order->payment_method = $this->payment; $this->square = new Square($this->payment); }); +function setupSquareClient(): \Square\SquareClient +{ + $clientBuilder = mock(SquareClientBuilder::class)->makePartial(); + app()->instance(SquareClientBuilder::class, $clientBuilder); + $client = Mockery::mock(SquareClient::class); + $clientBuilder->shouldReceive('build')->andReturn($client); + return $client; +} + +function setupSuccessfulPayment(SquareClient $client): void +{ + $response = Mockery::mock(ApiResponse::class); + $response->shouldReceive('isSuccess')->andReturn(true); + $response->shouldReceive('getResult')->andReturn(['payment' => 'success']); + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $paymentsApi->shouldReceive('createPayment')->andReturn($response); +} + it('returns correct payment form view for square', function() { expect(Square::$paymentFormView)->toBe('igniter.payregister::_partials.square.payment_form'); }); @@ -114,49 +133,171 @@ }); it('processes square payment form and returns success', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $order->totals()->create(['code' => 'tip', 'title' => 'Tip', 'value' => 100]); - $response = Mockery::mock(ApiResponse::class); + $client = setupSquareClient(); + setupSuccessfulPayment($client); + + $this->square->processPaymentForm([ + 'square_card_nonce' => 'nonce', + 'square_card_token' => 'token', + ], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('processes square payment form with new payment profile and returns success', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $client = setupSquareClient(); + + $customersApi = Mockery::mock(CustomersApi::class); + $client->shouldReceive('getCustomersApi')->andReturn($customersApi); + $createCustomerResponse = mock(ApiResponse::class); + $customersApi->shouldReceive('createCustomer')->andReturn($createCustomerResponse); + $createCustomerResponse->shouldReceive('isSuccess')->andReturn(true); + $createCustomerResponse->shouldReceive('getResult')->andReturnSelf(); + $customerObject = mock(\Square\Models\Customer::class)->makePartial(); + $customerObject->shouldReceive('getId')->andReturn('cust123'); + $customerObject->shouldReceive('getReferenceId')->andReturn('ref123'); + $createCustomerResponse->shouldReceive('getCustomer')->andReturn($customerObject); + + $cardsApi = Mockery::mock(CardsApi::class); + $client->shouldReceive('getCardsApi')->andReturn($cardsApi); + $createCardResponse = mock(ApiResponse::class); + $cardsApi->shouldReceive('createCard')->andReturn($createCardResponse); + $createCardResponse->shouldReceive('isSuccess')->andReturn(true); + $createCardResponse->shouldReceive('getResult')->andReturnSelf(); + $cardObject = mock(\Square\Models\Card::class)->makePartial(); + $cardObject->shouldReceive('getId')->andReturn('card123'); + $createCardResponse->shouldReceive('getCard')->andReturn($cardObject); + + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $response = mock(ApiResponse::class); $response->shouldReceive('isSuccess')->andReturn(true); $response->shouldReceive('getResult')->andReturn(['payment' => 'success']); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('createPayment')->andReturn($response); - - $square->processPaymentForm(['square_card_nonce' => 'nonce'], $this->payment, $this->order); -}); - -it('processes square payment form with payment profile and returns success', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $this->order->shouldReceive('updateOrderStatus')->once(); - $paymentProfile = Mockery::mock(PaymentProfile::class)->makePartial(); - $paymentProfile->profile_data = ['card_id' => '123', 'customer_id' => '456']; - $response = Mockery::mock(ApiResponse::class); + $paymentsApi->shouldReceive('createPayment')->andReturn($response); + + $this->square->processPaymentForm([ + 'create_payment_profile' => 1, + 'square_card_nonce' => 'nonce', + 'first_name' => 'John', + 'last_name' => 'Doe', + ], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('processes square payment form with existing payment profile and returns success', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card123', 'customer_id' => 'cust123'], + ]); + $client = setupSquareClient(); + + $customersApi = Mockery::mock(CustomersApi::class); + $client->shouldReceive('getCustomersApi')->andReturn($customersApi); + $retrieveCustomerResponse = mock(ApiResponse::class); + $retrieveCustomerResponse->shouldReceive('isSuccess')->andReturn(true); + $customersApi->shouldReceive('retrieveCustomer')->andReturn($retrieveCustomerResponse); + $retrieveCustomerResponse->shouldReceive('getResult')->andReturnSelf(); + $customerObject = mock(\Square\Models\Customer::class)->makePartial(); + $customerObject->shouldReceive('getId')->andReturn('cust123'); + $customerObject->shouldReceive('getReferenceId')->andReturn('ref123'); + $retrieveCustomerResponse->shouldReceive('getCustomer')->andReturn($customerObject); + + $cardsApi = Mockery::mock(CardsApi::class); + $client->shouldReceive('getCardsApi')->andReturn($cardsApi); + $retrieveCardResponse = mock(ApiResponse::class); + $cardsApi->shouldReceive('retrieveCard')->andReturn($retrieveCardResponse); + $retrieveCardResponse->shouldReceive('isSuccess')->andReturn(true); + $retrieveCardResponse->shouldReceive('getResult')->andReturnSelf(); + $cardObject = mock(\Square\Models\Card::class)->makePartial(); + $cardObject->shouldReceive('getId')->andReturn('card123'); + $retrieveCardResponse->shouldReceive('getCard')->andReturn($cardObject); + + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $response = mock(ApiResponse::class); $response->shouldReceive('isSuccess')->andReturn(true); $response->shouldReceive('getResult')->andReturn(['payment' => 'success']); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('createPayment')->andReturn($response); - $square->shouldReceive('updatePaymentProfile')->andReturn($paymentProfile); + $paymentsApi->shouldReceive('createPayment')->andReturn($response); - $square->processPaymentForm([ + $this->square->processPaymentForm([ 'create_payment_profile' => 1, 'square_card_nonce' => 'nonce', - ], $this->payment, $this->order); + 'first_name' => 'John', + 'last_name' => 'Doe', + ], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('throws exception if payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + + $client = setupSquareClient(); + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $paymentsApi->shouldReceive('createPayment')->andThrow(new \Exception('Payment error')); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later'); + + $this->square->processPaymentForm(['square_card_nonce' => 'nonce'], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + 'is_success' => 0, + ]); }); -it('throws exception if square payment creation fails', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->with('Payment error -> Payment error', 0, Mockery::any(), Mockery::any())->once(); +it('throws exception if payment response fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $errorMock = Mockery::mock(Error::class); $errorMock->shouldReceive('getDetail')->andReturn('Payment error'); @@ -164,180 +305,300 @@ $response->shouldReceive('isSuccess')->andReturn(false); $response->shouldReceive('getErrors')->andReturn([$errorMock]); $response->shouldReceive('getResult')->andReturn([]); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('createPayment')->andReturn($response); + + $client = setupSquareClient(); + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $paymentsApi->shouldReceive('createPayment')->andReturn($response); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later'); - $square->processPaymentForm(['square_card_nonce' => 'nonce'], $this->payment, $this->order); + $this->square->processPaymentForm(['square_card_nonce' => 'nonce'], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + 'is_success' => 0, + ]); +}); + +it('throws exception when createOrFetchCustomer fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['customer_id' => 'cust123', 'card_id' => 'card123'], + ]); + $client = setupSquareClient(); + $customersApi = Mockery::mock(CustomersApi::class); + $client->shouldReceive('getCustomersApi')->andReturn($customersApi); + $retrieveCustomerResponse = mock(ApiResponse::class); + $customersApi->shouldReceive('retrieveCustomer')->andReturn($retrieveCustomerResponse); + $retrieveCustomerResponse->shouldReceive('isSuccess')->andReturn(false); + $createCustomerResponse = mock(ApiResponse::class); + $customersApi->shouldReceive('createCustomer')->andReturn($createCustomerResponse); + $createCustomerResponse->shouldReceive('isSuccess')->andReturn(false); + $errorMock = Mockery::mock(Error::class); + $errorMock->shouldReceive('getDetail')->andReturn('Customer creation failed'); + $createCustomerResponse->shouldReceive('getErrors')->andReturn([$errorMock]); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Square Customer Create Error: Customer creation failed'); + + $this->square->processPaymentForm([ + 'create_payment_profile' => 1, + 'square_card_nonce' => 'nonce', + ], $this->payment, $order); +}); + +it('throws exception when createOrFetchCard fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['customer_id' => 'cust123', 'card_id' => 'card123'], + ]); + $client = setupSquareClient(); + $customersApi = Mockery::mock(CustomersApi::class); + $client->shouldReceive('getCustomersApi')->andReturn($customersApi); + $retrieveCustomerResponse = mock(ApiResponse::class); + $customersApi->shouldReceive('retrieveCustomer')->andReturn($retrieveCustomerResponse); + $retrieveCustomerResponse->shouldReceive('isSuccess')->andReturn(true); + $retrieveCustomerResponse->shouldReceive('getResult')->andReturnSelf(); + $customerObject = mock(\Square\Models\Customer::class)->makePartial(); + $customerObject->shouldReceive('getId')->andReturn('cust123'); + $customerObject->shouldReceive('getReferenceId')->andReturn('ref123'); + $retrieveCustomerResponse->shouldReceive('getCustomer')->andReturn($customerObject); + + $cardsApi = Mockery::mock(CardsApi::class); + $client->shouldReceive('getCardsApi')->andReturn($cardsApi); + $retrieveCardResponse = mock(ApiResponse::class); + $cardsApi->shouldReceive('retrieveCard')->andReturn($retrieveCardResponse); + $retrieveCardResponse->shouldReceive('isSuccess')->andReturn(false); + $createCardResponse = mock(ApiResponse::class); + $cardsApi->shouldReceive('createCard')->andReturn($createCardResponse); + $createCardResponse->shouldReceive('isSuccess')->andReturn(false); + $errorMock = Mockery::mock(Error::class); + $errorMock->shouldReceive('getDetail')->andReturn('Card creation failed'); + $createCardResponse->shouldReceive('getErrors')->andReturn([$errorMock]); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Square Create Payment Card Error: Card creation failed'); + + $this->square->processPaymentForm([ + 'create_payment_profile' => 1, + 'square_card_nonce' => 'nonce', + 'first_name' => 'John', + 'last_name' => 'Doe', + ], $this->payment, $order); +}); + +it('returns true when payment profiles are supported', function() { + $result = $this->square->supportsPaymentProfiles(); + + expect($result)->toBeTrue(); }); it('processes square refund form and logs refund attempt', function() { - $this->paymentLog->refunded_at = null; - $this->paymentLog->response = ['payment' => ['status' => 'COMPLETED', 'id' => 'payment_id']]; - $this->order->order_total = 100; - $this->order->shouldReceive('logPaymentAttempt')->with(Mockery::type('string'), 1, Mockery::any(), Mockery::any())->once(); - $this->paymentLog->shouldReceive('markAsRefundProcessed')->once(); + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['payment' => ['status' => 'COMPLETED', 'id' => 'payment_id']], + ]); + $client = setupSquareClient(); $response = Mockery::mock(ApiResponse::class); $response->shouldReceive('isSuccess')->andReturn(true); $response->shouldReceive('getResult')->andReturn(['id' => 'refund_id']); $refundsApi = Mockery::mock(RefundsApi::class); $refundsApi->shouldReceive('refundPayment')->andReturn($response)->once(); - $squareClient = Mockery::mock(SquareClient::class); - $squareClient->shouldReceive('getRefundsApi')->andReturn($refundsApi); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('createClient')->andReturn($squareClient); + $client->shouldReceive('getRefundsApi')->andReturn($refundsApi); + + $this->square->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); +}); + +it('throws exception when charge is already refunded', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['payment' => ['status' => 'not_completed']], + 'refunded_at' => now(), + ]); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Nothing to refund, payment already refunded'); - $square->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->square->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); }); -it('throws exception if no square charge to refund', function() { - $this->paymentLog->refunded_at = null; - $this->paymentLog->response = ['payment' => ['status' => 'not_completed']]; +it('throws exception when no square charge to refund', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['payment' => ['status' => 'not_completed']], + ]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No charge to refund'); - $this->square->processRefundForm(['refund_type' => 'full'], $this->order, $this->paymentLog); + $this->square->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); +}); + +it('throws exception when refund response fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'response' => ['payment' => ['status' => 'COMPLETED', 'id' => 'payment_id']], + ]); + + $client = setupSquareClient(); + $response = Mockery::mock(ApiResponse::class); + $response->shouldReceive('isSuccess')->andReturn(false); + $response->shouldReceive('getResult')->andReturn(['id' => 'refund_id']); + $refundsApi = Mockery::mock(RefundsApi::class); + $refundsApi->shouldReceive('refundPayment')->andReturn($response)->once(); + $client->shouldReceive('getRefundsApi')->andReturn($refundsApi); + + $this->square->bindEvent('square.extendRefundFields', function($fields, $order, $data) { + return [ + 'extra_field' => 'extra_value', + ]; + }); + + $this->square->processRefundForm(['refund_type' => 'full'], $order, $paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Refund failed -> Refund failed', + 'is_success' => 0, + ]); }); it('creates payment successfully from square payment profile', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), 'payment_id' => $this->payment->getKey(), 'profile_data' => ['card_id' => 'card123', 'customer_id' => 'cust123'], ]); - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer_name = 'John Doe'; - $this->order->customer->customer_id = 1; - $this->order->shouldReceive('logPaymentAttempt')->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $this->order->shouldReceive('updateOrderStatus')->once(); - $response = Mockery::mock(ApiResponse::class); - $response->shouldReceive('isSuccess')->andReturn(true); - $response->shouldReceive('getResult')->andReturn(['payment' => 'success']); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('getHostObject')->andReturn($this->payment); - $square->shouldReceive('createPayment')->andReturn($response); + $client = setupSquareClient(); + setupSuccessfulPayment($client); + + $this->square->payFromPaymentProfile($order, []); - $square->payFromPaymentProfile($this->order, []); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); }); -it('throws exception if square payment profile not found', function() { - $this->order->customer = null; +it('throws exception when no square payment profile is found', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Payment profile not found'); - $this->square->payFromPaymentProfile($this->order, []); + $this->square->payFromPaymentProfile($order, []); }); -it('throws exception if square payment profile has no data', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; +it('throws exception when payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card123', 'customer_id' => 'cust123'], ]); + $client = setupSquareClient(); + $paymentsApi = Mockery::mock(PaymentsApi::class); + $client->shouldReceive('getPaymentsApi')->andReturn($paymentsApi); + $paymentsApi->shouldReceive('createPayment')->andThrow(new \Exception('Payment error')); + $this->expectException(ApplicationException::class); - $this->expectExceptionMessage('Payment profile not found'); + $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later'); - $this->square->payFromPaymentProfile($this->order, []); -}); + $this->square->payFromPaymentProfile($order, []); -it('creates new square payment profile if none exists', function() { - $data = ['square_card_nonce' => 'nonce']; - $this->order->customer = $customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error -> Payment error', + 'is_success' => 0, + ]); +}); - $response = Mockery::mock(ApiResponse::class); - $response->shouldReceive('getCustomer->getId')->andReturn('cust123'); - $response->shouldReceive('getCustomer->getReferenceId')->andReturn('ref123'); - $response->shouldReceive('getCard->getId')->andReturn('card123'); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('getHostObject')->andReturn($this->payment); - $square->shouldReceive('createOrFetchCustomer')->andReturn($response); - $square->shouldReceive('createOrFetchCard')->andReturn($response); - $square->shouldReceive('updatePaymentProfileData')->with(Mockery::any(), [ - 'customer_id' => 'cust123', - 'card_id' => 'card123', - ], Mockery::any())->once(); - - $square->updatePaymentProfile($customer, $data); -}); - -it('updates existing square payment profile', function() { - $data = ['square_card_nonce' => 'nonce']; - $this->order->customer = $customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; +it('deletes payment profile successfully', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $customer = Customer::factory()->create(); $profile = PaymentProfile::factory()->create([ + 'customer_id' => $customer->getKey(), 'payment_id' => $this->payment->getKey(), - 'profile_data' => ['card_id' => 'card123', 'customer_id' => 'cust123'], + 'profile_data' => ['customer_id' => 'cust123', 'card_id' => 'card123'], ]); + $client = setupSquareClient(); + $cardsApi = Mockery::mock(CardsApi::class); + $client->shouldReceive('getCardsApi')->andReturn($cardsApi); + $response = mock(ApiResponse::class); + $cardsApi->shouldReceive('disableCard')->andReturn($response); + $response->shouldReceive('isSuccess')->andReturn(true); - $response = Mockery::mock(ApiResponse::class); - $response->shouldReceive('getCustomer->getId')->andReturn('cust123'); - $response->shouldReceive('getCustomer->getReferenceId')->andReturn('ref123'); - $response->shouldReceive('getCard->getId')->andReturn('card123'); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('getHostObject')->andReturn($this->payment); - $square->shouldReceive('createOrFetchCustomer')->andReturn($response); - $square->shouldReceive('createOrFetchCard')->andReturn($response); - $square->shouldReceive('updatePaymentProfileData')->with(Mockery::any(), [ - 'customer_id' => 'cust123', - 'card_id' => 'card123', - ], Mockery::any())->once(); - - $result = $square->updatePaymentProfile($customer, $data); - expect($result->getKey())->toBe($profile->getKey()); -}); - -it('throws exception if createOrFetchCustomer fails for square', function() { - $data = ['square_card_nonce' => 'nonce']; - $this->order->customer = $customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('getHostObject')->andReturn($this->payment); - $square->shouldReceive('createOrFetchCustomer')->andThrow(new ApplicationException('Customer creation failed')); + $result = $this->square->deletePaymentProfile($customer, $profile); - $this->expectException(ApplicationException::class); - $this->expectExceptionMessage('Customer creation failed'); - - $square->updatePaymentProfile($customer, $data); + expect($result)->toBeNull(); }); -it('throws exception if createOrFetchCard fails for square', function() { - $data = ['square_card_nonce' => 'nonce']; - $this->order->customer = $customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; - - $response = Mockery::mock(ApiResponse::class); - $response->shouldReceive('getCustomer->getId')->andReturn('cust123'); - $response->shouldReceive('getCustomer->getReferenceId')->andReturn('ref123'); - $square = Mockery::mock(Square::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - $square->shouldReceive('getHostObject')->andReturn($this->payment); - $square->shouldReceive('createOrFetchCustomer')->andReturn($response); - - $square->shouldReceive('createOrFetchCard')->andThrow(new ApplicationException('Card creation failed')); - - $this->expectException(ApplicationException::class); - $this->expectExceptionMessage('Card creation failed'); +it('throws exception when deleting payment profile fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_access_token = 'test_access_token'; + $customer = Customer::factory()->create(); + $profile = PaymentProfile::factory()->create([ + 'customer_id' => $customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['customer_id' => 'cust123', 'card_id' => 'card123'], + ]); + $client = setupSquareClient(); + $cardsApi = Mockery::mock(CardsApi::class); + $client->shouldReceive('getCardsApi')->andReturn($cardsApi); + $cardsApi->shouldReceive('disableCard')->andReturn($response = mock(ApiResponse::class)); + $response->shouldReceive('isSuccess')->andReturn(false); + $errorMock = Mockery::mock(Error::class); + $errorMock->shouldReceive('getDetail')->andReturn('Deleting card failed'); + $response->shouldReceive('getErrors')->andReturn([$errorMock]); - $square->updatePaymentProfile($customer, $data); + expect(fn() => $this->square->deletePaymentProfile($customer, $profile)) + ->toThrow(ApplicationException::class, 'Square Delete Payment Card Error: Deleting card failed'); }); diff --git a/tests/Payments/StripeTest.php b/tests/Payments/StripeTest.php index 71ad936..ba18cd2 100644 --- a/tests/Payments/StripeTest.php +++ b/tests/Payments/StripeTest.php @@ -9,25 +9,35 @@ use Igniter\PayRegister\Models\Payment; use Igniter\PayRegister\Models\PaymentLog; use Igniter\PayRegister\Models\PaymentProfile; -use Igniter\PayRegister\Payments\PaypalExpress; use Igniter\PayRegister\Payments\Stripe; use Igniter\User\Models\Customer; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; use Mockery; -use Stripe\Service\PaymentIntentService; -use Stripe\StripeClient; +use Stripe\ApiRequestor as StripeApiRequestorAlias; +use Stripe\HttpClient\CurlClient; use Stripe\StripeObject; +use Stripe\Util\CaseInsensitiveArray; beforeEach(function() { $this->payment = Payment::factory()->create([ - 'class_name' => PaypalExpress::class, + 'class_name' => Stripe::class, ]); - $this->profile = Mockery::mock(PaymentProfile::class)->makePartial(); - $this->order = Mockery::mock(Order::class)->makePartial(); - $this->order->payment_method = $this->payment; $this->stripe = new Stripe($this->payment); + StripeApiRequestorAlias::setHttpClient($this->httpClient = mock(CurlClient::class)->makePartial()); }); +function setupRequest(CurlClient $httpClient, string $uri, array $response, string $method = 'get', int $statusCode = 200): void +{ + $httpClient->shouldReceive('request') + ->with($method, 'https://api.stripe.com/v1/'.$uri, Mockery::any(), Mockery::any(), false) + ->andReturn([ + json_encode($response), + 200, + new CaseInsensitiveArray(['Request-Id' => 'req_123']), + ]); +} + it('returns correct payment form view for stripe', function() { expect(Stripe::$paymentFormView)->toBe('igniter.payregister::_partials.stripe.payment_form'); }); @@ -111,352 +121,784 @@ it('creates stripe payment intent successfully', function() { $this->payment->transaction_mode = 'test'; $this->payment->test_secret_key = 'test_secret_key'; - $this->order->order_total = 100; - - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + setupRequest($this->httpClient, 'customers', [ + 'id' => 'cus_123', + ], 'post'); + setupRequest($this->httpClient, 'payment_intents', [ + 'id' => 'pi_123', + 'client_secret' => 'secret', + ], 'post'); + + expect($this->stripe->createOrFetchIntent($order))->toBe('secret'); +}); - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false)->once(); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('validateApplicableFee')->with($this->order, $this->payment)->once(); - $stripe->shouldReceive('updatePaymentIntentSession')->with($this->order)->andReturn(null)->once(); - $stripe->shouldReceive('getPaymentFormFields')->with($this->order)->andReturn(['amount' => 1000])->once(); - $stripe->shouldReceive('getStripeOptions')->andReturn([])->once(); - $gateway->shouldReceive('request')->andReturn(StripeObject::constructFrom(['id' => 'pi_123', 'client_secret' => 'secret']))->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway)->once(); +it('fetches & updates stripe payment intent successfully', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + setupRequest($this->httpClient, 'payment_intents/pi_123', [ + 'id' => 'pi_123', + 'status' => 'not-succeeded', + ]); + setupRequest($this->httpClient, 'payment_intents', [ + 'id' => 'pi_123', + 'client_secret' => 'secret', + ], 'post'); - expect($stripe->createOrFetchIntent($this->order))->toBe('secret'); + expect($this->stripe->createOrFetchIntent($order))->toBe('secret'); }); -it('fetches & updates stripe payment intent successfully', function() { - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway = Mockery::mock(StripeClient::class); - $gateway->paymentIntents = $paymentIntents; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false)->once(); - $paymentIntents->shouldReceive('retrieve') - ->with('pi_123', Mockery::any(), Mockery::any()) - ->andReturn(StripeObject::constructFrom(['id' => 'pi_123', 'status' => 'not_succeeded']))->once(); - $paymentIntents->shouldReceive('update') - ->with('pi_123', Mockery::on(function($data) { - return empty(array_only((array)$data, ['capture_method', 'setup_future_usage', 'customer'])); - }), Mockery::any()) - ->andReturn(StripeObject::constructFrom(['id' => 'pi_123', 'status' => 'not_succeeded', 'client_secret' => 'secret']))->once(); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('validateApplicableFee')->with($this->order, $this->payment)->once(); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('getPaymentFormFields')->with($this->order, Mockery::any(), true)->andReturn(['amount' => 1000])->once(); - $stripe->shouldReceive('getStripeOptions')->andReturn([])->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway)->once(); - - expect($stripe->createOrFetchIntent($this->order))->toBe('secret'); -}); - -it('returns null if payment is already processed for stripe', function() { - $order = Mockery::mock(Order::class); - $order->shouldReceive('isPaymentProcessed')->andReturn(true); +it('returns null when payment is already processed in createOrFetchIntent', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create([ + 'order_total' => 100, + 'processed' => 1, + ]); + $order->updateOrderStatus(1); $result = $this->stripe->createOrFetchIntent($order); expect($result)->toBeNull(); }); -it('logs error and returns null if exception occurs in createOrFetchIntent', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); +it('logs error and returns null when exception occurs in createOrFetchIntent', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $this->httpClient->shouldReceive('request')->andThrow(new Exception('Error')); - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); - $this->order->shouldReceive('logPaymentAttempt')->with('Creating checkout session failed: Error', 0, [], []); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('validateApplicableFee')->andThrow(new Exception('Error')); + expect($this->stripe->createOrFetchIntent($order))->toBeNull(); - expect($stripe->createOrFetchIntent($this->order))->toBeNull() - ->and(flash()->messages()->first())->message->not->toBeNull()->level->toBe('warning'); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Creating checkout session failed: Error', + 'is_success' => 0, + ]); }); it('processes stripe payment form successfully', function() { - $data = []; - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway = Mockery::mock(StripeClient::class); - $gateway->paymentIntents = $paymentIntents; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, $data, Mockery::any(), true)->once(); - $this->order->shouldReceive('updateOrderStatus')->with($this->payment->order_status, ['notify' => false])->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - $paymentIntents->shouldReceive('retrieve')->andReturn(StripeObject::constructFrom([ + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + setupRequest($this->httpClient, 'payment_intents/pi_123', [ + 'id' => 'pi_123', 'status' => 'succeeded', - 'payment_method' => (object)[ - 'id' => 'pm_123', - 'card' => (object)[], - ], - ])); - $stripe->shouldReceive('validateApplicableFee')->with($this->order, $this->payment); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $stripe->shouldReceive('forgetSession')->with('ti_payregister_stripe_intent')->once(); + ]); + + $data = []; + $result = $this->stripe->processPaymentForm($data, $this->payment, $order); + + expect($result)->toBeNull() + ->and($this->stripe->getSession('ti_payregister_stripe_intent'))->toBeNull(); - expect($stripe->processPaymentForm($data, $this->payment, $this->order))->toBeNull(); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); }); -it('throws exception if stripe payment intent id is missing in session', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); +it('returns true when payment is already processed in processPaymentForm', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create([ + 'order_total' => 100, + 'processed' => 1, + ]); + $order->updateOrderStatus(1); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + + $result = $this->stripe->processPaymentForm([], $this->payment, $order); + expect($result)->toBeTrue(); +}); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment error: Missing payment intent identifier in session.', 0, [], [])->once(); - $stripe->shouldReceive('validateApplicableFee')->with($this->order, $this->payment); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturnNull()->once(); +it('throws exception if stripe payment intent id is missing in session', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); - $stripe->processPaymentForm([], $this->payment, $this->order); + $this->stripe->processPaymentForm([], $this->payment, $order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error: Missing payment intent identifier in session.', + 'is_success' => 0, + ]); }); -it('logs error and throws exception if retrieving stripe payment intent fails', function() { +it('logs error and throws exception when payment intent status is not succeeded', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory()->for($this->payment, 'payment_method')->create(['order_total' => 100]); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + setupRequest($this->httpClient, 'payment_intents/pi_123', [ + 'id' => 'pi_123', + 'status' => 'not_succeeded', + ]); + $data = []; - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway = Mockery::mock(StripeClient::class); - $gateway->paymentIntents = $paymentIntents; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - - $this->order->shouldReceive('isPaymentProcessed')->andReturn(false); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment error: Error', 0, $data, Mockery::any())->once(); - $paymentIntents->shouldReceive('retrieve')->andThrow(new Exception('Error')); - $stripe->shouldReceive('validateApplicableFee')->with($this->order, $this->payment); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $this->expectException(ApplicationException::class); - $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); + $result = $this->stripe->processPaymentForm($data, $this->payment, $order); - $stripe->processPaymentForm($data, $this->payment, $this->order); + expect($result)->toBeTrue(); }); -it('captures authorized stripe payment successfully', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $paymentLog->response = ['id' => 'pi_123']; - $gateway = Mockery::mock(StripeClient::class); - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; +it('updates payment profile on process payment form success', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + setupRequest($this->httpClient, 'payment_intents/pi_123', [ + 'id' => 'pi_123', + 'status' => 'requires_capture', + 'payment_method' => [ + 'id' => 'pm_123', + 'card' => StripeObject::constructFrom(['brand' => 'Visa', 'last4' => '4242']), + ], + ]); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $this->order->shouldReceive('payment')->andReturn($this->payment->code); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $paymentIntents->shouldReceive('capture')->andReturn(StripeObject::constructFrom(['status' => 'succeeded'])); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, $data, Mockery::any())->once(); + $this->stripe->processPaymentForm([ + 'create_payment_profile' => 1, + ], $this->payment, $order); - $result = $stripe->captureAuthorizedPayment($this->order, $data); - expect($result)->status->toBe('succeeded'); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment authorized', + 'is_success' => 1, + 'is_refundable' => 0, + ]); }); -it('throws exception if no successful authorized stripe payment to capture', function() { - $data = []; +it('captures authorized payment successfully', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + setupRequest($this->httpClient, 'payment_intents/pi_123/capture', [ + 'id' => 'pi_123', + 'status' => 'succeeded', + ], 'post'); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn(null); + $this->stripe->bindEvent('stripe.extendCaptureFields', function($data, $order) { + return [ + 'extra_field' => 'extra_value', + ]; + }); + + $this->stripe->captureAuthorizedPayment($order, []); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('throws exception when no successful authorized payment to capture', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 0, + 'response' => ['id' => 'pi_123'], + ]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No successful authorized payment to capture'); - $this->stripe->captureAuthorizedPayment($this->order, $data); + $this->stripe->captureAuthorizedPayment($order, []); }); -it('throws exception if missing stripe payment intent id in successful authorized payment response', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $paymentLog->response = []; +it('throws exception when payment intent id is missing in payment response', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['status' => 'succeeded'], + ]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Missing payment intent ID in successful authorized payment response'); - $this->stripe->captureAuthorizedPayment($this->order, $data); + $this->stripe->captureAuthorizedPayment($order, []); }); -it('logs error if exception occurs in captureAuthorizedPayment for stripe', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $paymentLog->response = ['id' => 'pi_123']; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; +it('logs error when capture authorized payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + $this->httpClient->shouldReceive('request')->andThrow(new Exception('Error')); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $this->order->shouldReceive('payment')->andReturn($this->payment->code); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $paymentIntents->shouldReceive('capture')->andThrow(new Exception('Error')); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment capture failed: Error', 0, $data)->once(); + expect($this->stripe->captureAuthorizedPayment($order))->toBeNull(); - expect($stripe->captureAuthorizedPayment($this->order, $data))->toBeNull(); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment capture failed: Error', + 'is_success' => 0, + ]); +}); + +it('logs error when capture authorized payment response is invalid', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + setupRequest($this->httpClient, 'payment_intents/pi_123/capture', [ + 'id' => 'pi_123', + 'status' => 'invalid', + ], 'post'); + + $this->stripe->captureAuthorizedPayment($order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment capture failed', + 'is_success' => 0, + ]); }); it('cancels authorized stripe payment successfully', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $paymentLog->response = ['id' => 'pi_123']; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + setupRequest($this->httpClient, 'payment_intents/pi_123/cancel', [ + 'id' => 'pi_123', + 'status' => 'canceled', + ], 'post'); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $this->order->shouldReceive('payment')->andReturn($this->payment->code); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $paymentIntents->shouldReceive('cancel')->andReturn((object)['status' => 'canceled']); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment canceled successfully', 1, $data, Mockery::any())->once(); + $this->stripe->bindEvent('stripe.extendCancelFields', function($data, $order) { + return [ + 'extra_field' => 'extra_value', + ]; + }); + + $this->stripe->cancelAuthorizedPayment($order); - expect($stripe->cancelAuthorizedPayment($this->order, $data))->status->toBe('canceled'); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment canceled successfully', + 'is_success' => 1, + ]); }); -it('throws exception if no successful authorized stripe payment to cancel', function() { - $data = []; - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn(null); +it('throws exception when no successful authorized payment to cancel', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('No successful authorized payment to cancel'); - $this->stripe->cancelAuthorizedPayment($this->order, $data); + $this->stripe->cancelAuthorizedPayment($order); }); -it('throws exception if missing stripe payment intent id in successful authorized payment response for cancel', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $paymentLog->response = []; +it('throws exception when missing payment intent id in cancel payment response', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => [], + ]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Missing payment intent ID in successful authorized payment response'); - $this->stripe->cancelAuthorizedPayment($this->order, $data); + $this->stripe->cancelAuthorizedPayment($order); }); -it('logs error if exception occurs in cancelAuthorizedPayment for stripe', function() { - $data = []; - $paymentLog = Mockery::mock(PaymentLog::class)->makePartial(); - $paymentLog->response = ['id' => 'pi_123']; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; +it('logs error when canceling authorized payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + $this->httpClient->shouldReceive('request')->andThrow(new Exception('Error')); - $this->order->shouldReceive('payment_logs->firstWhere')->with('is_success', true)->andReturn($paymentLog); - $this->order->shouldReceive('payment')->andReturn($this->payment->code); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $gateway->paymentIntents->shouldReceive('cancel')->andThrow(new Exception('Error')); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment canceled failed: Error', 0, $data)->once(); + $this->stripe->cancelAuthorizedPayment($order); - expect($stripe->cancelAuthorizedPayment($this->order, $data))->toBeNull(); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment canceled failed: Error', + 'is_success' => 0, + ]); }); -it('updates stripe payment intent session successfully', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; +it('logs error when canceling authorized payment response is invalid', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'response' => ['id' => 'pi_123'], + ]); + setupRequest($this->httpClient, 'payment_intents/pi_123/cancel', [ + 'id' => 'pi_123', + 'status' => 'invalid', + ], 'post'); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $paymentIntents->shouldReceive('retrieve')->andReturn(StripeObject::constructFrom(['id' => 'pi_123', 'status' => 'requires_payment_method']))->once(); - $stripe->shouldReceive('getPaymentFormFields')->with($this->order, [], true)->andReturn(['amount' => 1000]); - $stripe->shouldReceive('getStripeOptions')->andReturn([]); - $paymentIntents->shouldReceive('update')->andReturn(StripeObject::constructFrom(['id' => 'pi_123']))->once(); + $this->stripe->cancelAuthorizedPayment($order); - expect($stripe->updatePaymentIntentSession($this->order))->id->toBe('pi_123'); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Canceling payment failed', + 'is_success' => 0, + ]); }); it('returns stripe payment intent if status is requires_capture or succeeded', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; - - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $gateway->paymentIntents->shouldReceive('retrieve')->andReturn((object)['status' => 'requires_capture']); + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create()) + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $this->stripe->putSession('ti_payregister_stripe_intent', 'pi_123'); + setupRequest($this->httpClient, 'payment_intents/pi_123', [ + 'id' => 'pi_123', + 'status' => 'succeeded', + ]); - expect($stripe->updatePaymentIntentSession($this->order))->status->toBe('requires_capture'); + expect($this->stripe->updatePaymentIntentSession($order)->status)->toBe('succeeded'); }); -it('logs error and returns false if exception occurs in updatePaymentIntentSession', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; +it('deletes existing payment profile', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $customer = Customer::factory()->create(); + $profile = PaymentProfile::factory()->create([ + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card_123', 'customer_id' => 'cus_123'], + ]); + setupRequest($this->httpClient, 'customers/cus_123', [ + 'id' => 'cus_123', + ], 'delete'); - $stripe->shouldReceive('getSession')->with('ti_payregister_stripe_intent')->andReturn('pi_123')->once(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $gateway->paymentIntents->shouldReceive('retrieve')->andThrow(new Exception('Error')); - $this->order->shouldReceive('logPaymentAttempt')->with('Updating checkout session failed: Error', 1, [], Mockery::any())->once(); + $this->stripe->deletePaymentProfile($customer, $profile); - expect($stripe->updatePaymentIntentSession($this->order))->toBeFalse(); + $this->assertDatabaseHas('payment_profiles', ['payment_profile_id' => $profile->payment_profile_id]); }); -it('throws exception if stripe payment profile not found', function() { - $this->order->customer = null; +it('throws exception when customer payment profile not found', function() { + $order = Order::factory() + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Payment profile not found or customer not logged in'); - $this->stripe->payFromPaymentProfile($this->order, []); + $this->stripe->payFromPaymentProfile($order); }); -it('throws exception if stripe payment profile has no data', function() { - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer->customer_id = 1; +it('creates payment successfully from stripe payment profile', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); PaymentProfile::factory()->create([ - 'customer_id' => 1, + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card_123', 'customer_id' => 'cus_123'], + ]); + setupRequest($this->httpClient, 'customers/cus_123', [ + 'id' => 'cus_123', + 'deleted' => true, ]); + setupRequest($this->httpClient, 'customers', [ + 'id' => 'cus_123', + ], 'post'); + setupRequest($this->httpClient, 'payment_intents', [ + 'id' => 'pi_123', + 'status' => 'succeeded', + ], 'post'); - $this->expectException(ApplicationException::class); - $this->expectExceptionMessage('Payment profile not found or customer not logged in'); + $this->stripe->payFromPaymentProfile($order); - $this->stripe->payFromPaymentProfile($this->order, []); + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment successful', + 'is_success' => 1, + 'is_refundable' => 1, + ]); }); -it('creates payment successfully from stripe payment profile', function() { - $data = []; - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; - - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('paymentProfileExists')->andReturnTrue(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $stripe->shouldReceive('getStripeOptions')->andReturn([])->once(); - $stripe->shouldReceive('getPaymentFormFields')->with($this->order)->andReturn(['amount' => 1000])->once(); - $paymentIntents->shouldReceive('create')->andReturn(StripeObject::constructFrom(['status' => 'succeeded']))->once(); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment successful', 1, Mockery::any(), Mockery::any(), true)->once(); - $this->order->shouldReceive('updateOrderStatus')->with($this->payment->order_status, ['notify' => false])->once(); - $this->order->shouldReceive('markAsPaymentProcessed')->once(); - - $stripe->payFromPaymentProfile($this->order, $data); -}); - -it('logs payment attempt and throws exception if stripe payment creation fails', function() { - $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); - $gateway = Mockery::mock(StripeClient::class); - $paymentIntents = Mockery::mock(PaymentIntentService::class); - $gateway->paymentIntents = $paymentIntents; - - $this->order->customer = Mockery::mock(Customer::class)->makePartial(); - $this->order->customer_name = 'John Doe'; - $stripe->shouldReceive('getHostObject')->andReturn($this->payment); - $stripe->shouldReceive('paymentProfileExists')->andReturnTrue(); - $stripe->shouldReceive('createGateway')->andReturn($gateway); - $stripe->shouldReceive('getPaymentFormFields')->with($this->order)->andReturn(['amount' => 1000])->once(); - $paymentIntents->shouldReceive('create')->andThrow(new Exception('Payment error')); - $this->order->shouldReceive('logPaymentAttempt')->with('Payment error: Payment error', 0, Mockery::any(), Mockery::any())->once(); +it('logs payment attempt and throws exception when payment request fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card_123', 'customer_id' => 'cus_123'], + ]); + setupRequest($this->httpClient, 'customers/cus_123', [ + 'id' => 'cus_123', + 'deleted' => true, + ]); + setupRequest($this->httpClient, 'customers', [ + 'id' => 'cus_123', + ], 'post'); + setupRequest($this->httpClient, 'payment_intents', [ + 'id' => 'pi_123', + 'status' => 'invalid', + ], 'post'); + +// $stripe = Mockery::mock(Stripe::class)->makePartial()->shouldAllowMockingProtectedMethods(); +// $gateway = Mockery::mock(StripeClient::class); +// $paymentIntents = Mockery::mock(PaymentIntentService::class); +// $gateway->paymentIntents = $paymentIntents; +// +// $this->order->customer = Mockery::mock(Customer::class)->makePartial(); +// $this->order->customer_name = 'John Doe'; +// $stripe->shouldReceive('getHostObject')->andReturn($this->payment); +// $stripe->shouldReceive('paymentProfileExists')->andReturnTrue(); +// $stripe->shouldReceive('createGateway')->andReturn($gateway); +// $stripe->shouldReceive('getPaymentFormFields')->with($this->order)->andReturn(['amount' => 1000])->once(); +// $paymentIntents->shouldReceive('create')->andThrow(new Exception('Payment error')); +// $this->order->shouldReceive('logPaymentAttempt')->with('Payment error: Payment error', 0, Mockery::any(), Mockery::any())->once(); $this->expectException(ApplicationException::class); $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later'); - $stripe->payFromPaymentProfile($this->order, []); + $this->stripe->payFromPaymentProfile($order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error: Status invalid', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('throw exception when fails to create stripe customer request fails ', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + PaymentProfile::factory()->create([ + 'customer_id' => $order->customer->getKey(), + 'payment_id' => $this->payment->getKey(), + 'profile_data' => ['card_id' => 'card_123', 'customer_id' => 'cus_123'], + ]); + $this->httpClient->shouldReceive('request') + ->with('get', 'https://api.stripe.com/v1/customers/cus_123', Mockery::any(), Mockery::any(), false) + ->andThrow(new Exception('Error')); + + $this->httpClient->shouldReceive('request') + ->with('post', 'https://api.stripe.com/v1/customers', Mockery::any(), Mockery::any(), false) + ->andThrow(new Exception('Creating customer failed')); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Sorry, there was an error processing your payment. Please try again later.'); + + $this->stripe->payFromPaymentProfile($order); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment error: Creating customer failed', + 'is_success' => 0, + ]); +}); + +it('processes refund form and logs refund attempt', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'is_refundable' => 1, + 'response' => ['id' => 'pi_123', 'status' => 'succeeded', 'object' => 'payment_intent'], + ]); + setupRequest($this->httpClient, 'refunds', [ + 'id' => 're_123', + 'status' => 'succeeded', + ], 'post'); + + $this->stripe->bindEvent('stripe.extendRefundFields', function($data, $order) { + return [ + 'extra_field' => 'extra_value', + ]; + }); + + $this->stripe->processRefundForm([ + 'refund_type' => 'full', + ], $order, $paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Payment intent pi_123 refunded successfully -> (full: re_123)', + 'is_success' => 1, + ]); + +}); + +it('throws exception when stripe charge is already refunded', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'is_refundable' => 1, + 'refunded_at' => now(), + ]); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('Nothing to refund, payment has already been refunded'); + + $this->stripe->processRefundForm([], $order, $paymentLog); +}); + +it('throws exception when no stripe charge to refund', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'is_refundable' => 1, + 'response' => ['status' => 'invalid'], + ]); + + $this->expectException(ApplicationException::class); + $this->expectExceptionMessage('No charge to refund'); + + $this->stripe->processRefundForm([], $order, $paymentLog); +}); + +it('throws exception when refund response fails', function() { + $this->payment->transaction_mode = 'test'; + $this->payment->test_secret_key = 'test_secret_key'; + $order = Order::factory() + ->for(Customer::factory()->create(), 'customer') + ->for($this->payment, 'payment_method') + ->create(['order_total' => 100]); + $paymentLog = PaymentLog::factory()->create([ + 'order_id' => $order->order_id, + 'is_success' => 1, + 'is_refundable' => 1, + 'response' => ['id' => 'pi_123', 'status' => 'succeeded', 'object' => 'payment_intent'], + ]); + setupRequest($this->httpClient, 'refunds', [ + 'id' => 're_123', + 'status' => 'failed', + ], 'post'); + + $this->stripe->processRefundForm([], $order, $paymentLog); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->order_id, + 'message' => 'Refund failed -> Refund failed', + 'is_success' => 0, + ]); +}); + +it('returns 400 when request method is not POST', function() { + $response = $this->stripe->processWebhookUrl(); + + expect($response->getStatusCode())->toBe(400) + ->and($response->getContent())->toBe('Request method must be POST'); +}); + +it('returns 400 when webhook secret is invalid', function() { + $request = Request::create('stripe_webhook', 'POST'); + app()->instance('request', $request); + + $response = $this->stripe->processWebhookUrl(); + + expect($response->getStatusCode())->toBe(400) + ->and($response->getContent())->toBe('Invalid webhook secret'); +}); + +it('returns 400 if webhook payload is missing event type', function() { + $this->payment->applyGatewayClass(); + $this->payment->test_webhook_secret = $webhookSecret = 'whsec_test_webhook_secret'; + $this->payment->save(); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => '', + ]; + + $timestamp = time(); + $payloadJson = json_encode($payload); + $signature = hash_hmac('sha256', "{$timestamp}.{$payloadJson}", $webhookSecret); + + $response = $this->postJson('/ti_payregister/stripe_webhook/handle', $payload, [ + 'Stripe-Signature' => "t={$timestamp},v1={$signature}", + ]); + + expect($response->getStatusCode())->toBe(400) + ->and($response->getContent())->toBe('Missing webhook event name'); +}); + +it('handles webhook event and logs payment successful attempt', function() { + Event::fake(['payregister.stripe.webhook.handlePaymentIntentSucceeded']); + $order = Order::factory()->for($this->payment, 'payment_method')->create(); + $this->payment->applyGatewayClass(); + $this->payment->test_webhook_secret = $webhookSecret = 'whsec_test_webhook_secret'; + $this->payment->save(); + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'payment_intent.succeeded', + 'data' => [ + 'object' => [ + 'status' => 'succeeded', + 'metadata' => [ + 'order_id' => $order->getKey(), + ], + ], + ], + ]; + $timestamp = time(); + $payloadJson = json_encode($payload); + $signature = hash_hmac('sha256', "{$timestamp}.{$payloadJson}", $webhookSecret); + + $response = $this->postJson('/ti_payregister/stripe_webhook/handle', $payload, [ + 'Stripe-Signature' => "t={$timestamp},v1={$signature}", + ]); + + expect($response->getStatusCode())->toBe(200) + ->and($response->getContent())->toBe('Webhook Handled'); + + Event::assertDispatched('payregister.stripe.webhook.handlePaymentIntentSucceeded', function($eventName, $eventPayload) use ($order) { + return $eventPayload[0]['data']['object']['metadata']['order_id'] === $order->getKey(); + }); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->getKey(), + 'message' => 'Payment successful via webhook', + 'is_success' => 1, + 'is_refundable' => 1, + ]); +}); + +it('handles webhook event and logs payment authorized attempt', function() { + Event::fake(['payregister.stripe.webhook.handlePaymentIntentSucceeded']); + $order = Order::factory()->for($this->payment, 'payment_method')->create(); + $this->payment->applyGatewayClass(); + $this->payment->test_webhook_secret = $webhookSecret = 'whsec_test_webhook_secret'; + $this->payment->save(); + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'payment_intent.succeeded', + 'data' => [ + 'object' => [ + 'status' => 'requires_capture', + 'metadata' => [ + 'order_id' => $order->getKey(), + ], + ], + ], + ]; + $timestamp = time(); + $payloadJson = json_encode($payload); + $signature = hash_hmac('sha256', "{$timestamp}.{$payloadJson}", $webhookSecret); + + $response = $this->postJson('/ti_payregister/stripe_webhook/handle', $payload, [ + 'Stripe-Signature' => "t={$timestamp},v1={$signature}", + ]); + + expect($response->getStatusCode())->toBe(200) + ->and($response->getContent())->toBe('Webhook Handled'); + + Event::assertDispatched('payregister.stripe.webhook.handlePaymentIntentSucceeded', function($eventName, $eventPayload) use ($order) { + return $eventPayload[0]['data']['object']['metadata']['order_id'] === $order->getKey(); + }); + + $this->assertDatabaseHas('payment_logs', [ + 'order_id' => $order->getKey(), + 'message' => 'Payment authorized via webhook', + 'is_success' => 1, + 'is_refundable' => 0, + ]); });