diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..349f2b6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* sjelfull \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3210a74..3d4cfff 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- -name: Bug report -about: Create a bug report to help us improve +name: Bug Report +about: Report an issue or unexpected behavior title: '' labels: bug assignees: sjelfull @@ -10,21 +10,19 @@ assignees: sjelfull **Describe the bug** A clear and concise description of what the bug is. -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error +**Steps to reproduce** -**Expected behavior** -A clear and concise description of what you expected to happen. +1. +2. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Screenshots/Video** + +If applicable, add screenshots or videos to help explain your problem. **Additional info** -- Craft version: +- Craft CMS version: +- Craft Commerce version: +- Vipps for Craft Commerce version: - PHP version: - Database driver & version: - Plugins & versions: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8dd8c25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://github.com/superbigco/craft-vipps/discussions/new?category=ideas + about: Start a new discussion about your idea \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b0df4..775ba9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ## 1.0.7 - 2021-03-03 -> {warning} This release fixes a issue with completed orders not being marked as paid, so upgrading is strongly recommended. +> {warning} This release fixes an issue with completed orders not being marked as paid, so upgrading is strongly recommended. ### Added - Now sends the `Merchant-Serial-Number` header on all requests by default @@ -19,7 +19,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Fixed - Fixed deprecated shipping methods call - Fixed exception when capture was happening on status change and a transaction couldn't be found -- Fix wrong JSON response when returning from Vipps and a order already was paid +- Fix wrong JSON response when returning from Vipps and an order already was paid - Fix wrong method being used to complete order on successful payment. This could lead to orders being marked as not paid. - Fixed deprecated query builder method - Fixed type parity when comparing paid amount and order total ([#43](https://github.com/superbigco/craft-vipps/pull/43)) diff --git a/README.md b/README.md index 83fc6c6..4163ba4 100644 --- a/README.md +++ b/README.md @@ -95,20 +95,20 @@ This is an overview that shows you where to get the different config values thro ### Callback URLs -Note that all URLs passed to Vipps will have to be publicly available. Vipps will validate the URL, and return an error if its accessible from their servers. +Note that all URLs passed to Vipps will have to be publicly available. Vipps will validate the URL, and return an error if it is accessible from their servers. ## Using Vipps ## Express Checkout Buttons -Vipps allow a customer to check and pay for a order straight in the Vipps out, decreasing the number of steps a customer have to take to finish a order. +Vipps allow a customer to check and pay for an order straight in the Vipps out, decreasing the number of steps a customer have to take to finish an order. -To display a Express Checkout button for a variant: +To display an Express Checkout button for a variant: ```twig {{ craft.vipps.getExpressFormButton(variant) }} ``` -To display a Express Checkout button for a cart: +To display an Express Checkout button for a cart: ```twig {{ craft.vipps.getExpressFormButton() }} ``` diff --git a/composer.json b/composer.json index 91789ba..32530ca 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "superbig/craft-vipps", - "description": "Integrate Commerce with Vipps", + "description": "Integrate Craft Commerce 4.0+ with Vipps", "type": "craft-plugin", - "version": "1.0.8", + "version": "2.0.0", "keywords": [ "craft", "cms", @@ -10,7 +10,8 @@ "craft-plugin", "vipps", "craft commerce", - "commerce" + "commerce", + "gateway" ], "support": { "docs": "https://github.com/superbigco/craft-vipps/blob/master/README.md", @@ -23,16 +24,27 @@ "homepage": "https://superbig.co" } ], + "minimum-stability": "dev", + "prefer-stable": true, "require": { - "php": "8.0", + "php": "^8.0.2", "craftcms/cms": "^4.0.0", "craftcms/commerce": "^4.0.0" }, + "require-dev": { + "craftcms/phpstan": "dev-main", + "craftcms/ecs": "dev-main" + }, "autoload": { "psr-4": { "superbig\\vipps\\": "src/" } }, + "scripts": { + "phpstan": "phpstan analyse --memory-limit=1G", + "check-cs": "ecs check --ansi", + "fix-cs": "ecs check --ansi --fix" + }, "extra": { "name": "Vipps", "handle": "vipps", @@ -40,5 +52,14 @@ "hasCpSection": false, "changelogUrl": "https://raw.githubusercontent.com/superbigco/craft-vipps/master/CHANGELOG.md", "class": "superbig\\vipps\\Vipps" + }, + "config": { + "allow-plugins": { + "yiisoft/yii2-composer": true, + "craftcms/plugin-installer": true + }, + "platform": { + "php": "8.0.2" + } } } diff --git a/ecs.php b/ecs.php new file mode 100644 index 0000000..7b09884 --- /dev/null +++ b/ecs.php @@ -0,0 +1,14 @@ +paths([ + __DIR__ . '/src', + __FILE__, + ]); + + $ecsConfig->parallel(); + $ecsConfig->sets([SetList::CRAFT_CMS_4]); +}; \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..6189494 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,14 @@ +includes: + - %currentWorkingDirectory%/vendor/craftcms/phpstan/phpstan.neon +parameters: + level: 5 + paths: + - src + scanFiles: + # This file must be added to the project root: + # https://github.com/craftcms/cms/blob/develop/lib/craft/behaviors/CustomFieldBehavior.php + - %currentWorkingDirectory%/lib/craft/behaviors/CustomFieldBehavior.php + bootstrapFiles: + - %currentWorkingDirectory%/bootstrap.php + ignoreErrors: + - '#Call to an undefined method yii\\web\\UrlManager::setRouteParams\(\)#' \ No newline at end of file diff --git a/src/Services.php b/src/Services.php index 3cb895f..2ad61ee 100644 --- a/src/Services.php +++ b/src/Services.php @@ -10,6 +10,8 @@ namespace superbig\vipps; +use Craft; +use superbig\vipps\records\PaymentRecord; use superbig\vipps\services\Api; use superbig\vipps\services\Express; use superbig\vipps\services\Payments; @@ -20,12 +22,12 @@ * @package superbig\vipps * * @property Payments $payments - * @property Express $express - * @property Api $api + * @property Express $express + * @property Api $api */ trait Services { - public function initComponents() + public function initComponents(): void { $this->setComponents([ 'payments' => Payments::class, @@ -37,7 +39,7 @@ public function initComponents() /** * @return Payments The Payments service */ - public function getPayments() + public function getPayments(): Payments { return $this->get('payments'); } @@ -45,7 +47,7 @@ public function getPayments() /** * @return Api The Api service */ - public function getApi() + public function getApi(): Api { return $this->get('api'); } @@ -53,8 +55,28 @@ public function getApi() /** * @return Express The Express service */ - public function getExpress() + public function getExpress(): Express { return $this->get('express'); } + + /** + * Determine whether our table schema exists or not; this is needed because + * migrations such as the install migration and base_install migration may + * not have been run by the time our init() method has been called + * + * @return bool + */ + public function migrationsAndSchemaReady(): bool + { + $pluginsService = Craft::$app->getPlugins(); + if ($pluginsService->isPluginUpdatePending(self::$plugin)) { + return false; + } + if (Craft::$app->db->schema->getTableSchema(PaymentRecord::tableName()) === null) { + return false; + } + + return true; + } } diff --git a/src/Vipps.php b/src/Vipps.php index 141d761..1885561 100644 --- a/src/Vipps.php +++ b/src/Vipps.php @@ -28,7 +28,6 @@ use craft\web\UrlManager; use superbig\vipps\behaviors\TransactionBehavior; use superbig\vipps\gateways\Gateway; -use superbig\vipps\models\Settings; use superbig\vipps\variables\VippsVariable; use yii\base\Event; @@ -40,26 +39,19 @@ * @package Vipps * @since 1.0.0 * - * - * @method Settings getSettings() */ class Vipps extends Plugin { use Services; - // Static Properties - // ========================================================================= - + public bool $hasCpSettings = false; public static Vipps $plugin; - public static $commerceInstalled = false; + public static bool $commerceInstalled = false; public string $schemaVersion = '1.0.0'; - // Public Methods - // ========================================================================= - - public function init() + public function init(): void { parent::init(); self::$plugin = $this; @@ -81,28 +73,15 @@ public function init() ); } - protected function createSettingsModel(): ?Model - { - return new Settings(); - } - - - protected function settingsHtml(): string - { - return Craft::$app->view->renderTemplate( - 'vipps/settings', - [ - 'settings' => $this->getSettings(), - ] - ); - } - - protected function installEventListeners() + protected function installEventListeners(): void { - $this->installGlobalEventListeners(); + // Install our event listeners only if our table schema exists + if ($this->migrationsAndSchemaReady()) { + $this->installGlobalEventListeners(); + } } - public function installGlobalEventListeners() + public function installGlobalEventListeners(): void { Event::on( Gateways::class, @@ -157,7 +136,7 @@ function () { ); } - protected function installSiteEventListeners() + protected function installSiteEventListeners(): void { Event::on( UrlManager::class, diff --git a/src/assetbundles/vipps/VippsAsset.php b/src/assetbundles/vipps/VippsAsset.php index ffb6f14..1b0cf3c 100644 --- a/src/assetbundles/vipps/VippsAsset.php +++ b/src/assetbundles/vipps/VippsAsset.php @@ -24,7 +24,7 @@ class VippsAsset extends AssetBundle // ========================================================================= - public function init() + public function init(): void { $this->sourcePath = "@superbig/vipps/assetbundles/vipps/dist"; diff --git a/src/assetbundles/vippsexpress/VippsExpressAsset.php b/src/assetbundles/vippsexpress/VippsExpressAsset.php index 954250b..92b9bd8 100644 --- a/src/assetbundles/vippsexpress/VippsExpressAsset.php +++ b/src/assetbundles/vippsexpress/VippsExpressAsset.php @@ -24,7 +24,7 @@ class VippsExpressAsset extends AssetBundle // ========================================================================= - public function init() + public function init(): void { $this->sourcePath = "@superbig/vipps/assetbundles/vippsexpress/dist"; diff --git a/src/assetbundles/vippsexpress/dist/js/Vipps.js b/src/assetbundles/vippsexpress/dist/js/Vipps.js index 60e5021..6e72a9e 100644 --- a/src/assetbundles/vippsexpress/dist/js/Vipps.js +++ b/src/assetbundles/vippsexpress/dist/js/Vipps.js @@ -10,7 +10,7 @@ * @since 1.0.0 */ (function() { - var formUrl = '{{ siteUrl('vipps/express/checkout') }}'; + const formUrl = '{{ siteUrl('vipps/express/checkout') }}'; var formId = '{{ expressButtonId ~ 'Form' }}'; var $button = document.getElementById('{{ expressButtonId }}'); var clickHandler = (e) => { diff --git a/src/assetbundles/vippsutilityutility/VippsUtilityUtilityAsset.php b/src/assetbundles/vippsutilityutility/VippsUtilityUtilityAsset.php index 6b0dd57..dcd3e5c 100644 --- a/src/assetbundles/vippsutilityutility/VippsUtilityUtilityAsset.php +++ b/src/assetbundles/vippsutilityutility/VippsUtilityUtilityAsset.php @@ -24,7 +24,7 @@ class VippsUtilityUtilityAsset extends AssetBundle // ========================================================================= - public function init() + public function init(): void { $this->sourcePath = "@superbig/vipps/assetbundles/vippsutilityutility/dist"; diff --git a/src/behaviors/TransactionBehavior.php b/src/behaviors/TransactionBehavior.php index 10fe53c..e6efcab 100644 --- a/src/behaviors/TransactionBehavior.php +++ b/src/behaviors/TransactionBehavior.php @@ -28,7 +28,7 @@ class TransactionBehavior extends Behavior /** @var Transaction */ public $owner; - public function getVippsError() + public function getVippsError(): ?ErrorModel { $transaction = $this->owner; @@ -41,7 +41,7 @@ public function getVippsError() ]); } - public function isCancelled() + public function isCancelled(): bool { if (!$this->isVippsGateway()) { return false; @@ -55,7 +55,7 @@ public function getResponse() return Json::decode($this->owner->response); } - public function isVippsGateway() + public function isVippsGateway(): bool { return $this->owner->getGateway() instanceof Gateway; } diff --git a/src/controllers/CallbackController.php b/src/controllers/CallbackController.php index e5eb60f..ad179fb 100644 --- a/src/controllers/CallbackController.php +++ b/src/controllers/CallbackController.php @@ -1,4 +1,5 @@ -getOrder(); if ($order->getIsPaid()) { - return $this->redirect(Vipps::$plugin->payments->getFallbackUrl($order)); + return $this->redirect(Vipps::$plugin->getPayments()->getFallbackUrl($order)); } /** @var TransactionBehavior|Transaction $lastTransaction */ @@ -85,10 +86,10 @@ public function actionReturn(string $orderId) if ($error) { Craft::$app->getSession()->setError($error); - return $this->redirect(Vipps::$plugin->payments->getFallbackErrorUrl($order)); + return $this->redirect(Vipps::$plugin->getPayments()->getFallbackErrorUrl($order)); } - return $this->redirect(Vipps::$plugin->payments->getFallbackUrl($order)); + return $this->redirect(Vipps::$plugin->getPayments()->getFallbackUrl($order)); } /** @@ -136,24 +137,25 @@ public function actionComplete($orderId = null) if ($response->isExpress()) { // @todo This is hardcoded while Vipps only support Norway - $country = Plugin::getInstance()->getCountries()->getCountryByIso('NO'); $shippingDetails = $payload['shippingDetails']; $addressPayload = $shippingDetails['address']; $address = new Address([ 'firstName' => ArrayHelper::getValue($payload, 'userDetails.firstName'), 'lastName' => ArrayHelper::getValue($payload, 'userDetails.lastName'), - 'address1' => $addressPayload['addressLine1'] ?? null, - 'address2' => $addressPayload['addressLine2'] ?? null, - 'city' => $addressPayload['city'], - 'zipCode' => $addressPayload['zipCode'], - 'countryId' => $country->id, + 'fullName' => implode(' ', [ArrayHelper::getValue($payload, 'userDetails.firstName'), ArrayHelper::getValue($payload, 'userDetails.lastName')]), + 'addressLine1' => $addressPayload['addressLine1'] ?? null, + 'addressLine2' => $addressPayload['addressLine2'] ?? null, + 'locality' => $addressPayload['city'], + 'postalCode' => $addressPayload['zipCode'], + 'countryCode' => 'NO', + 'ownerId' => $order->id, ]); $order->setBillingAddress($address); $order->setShippingAddress($address); $order->shippingMethodHandle = $shippingDetails['shippingMethodId']; - if (!$order->getCustomer()->getUser() && empty($order->getEmail())) { + if (empty($order->getEmail())) { $order->setEmail($response->getEmail()); } @@ -249,26 +251,26 @@ public function actionShippingDetails(string $orderId = null) try { $order = $transaction->getOrder(); $isFirst = true; - $currentHandle = $order->shippingMethodHandle; + $currentHandle = $order->getShippingMethod()?->handle; // $iso = $payload['country']; - $country = Plugin::getInstance()->getCountries()->getCountryByIso('NO'); $address = new Address([ - 'address1' => $payload['addressLine1'] ?? null, - 'address2' => $payload['addressLine2'] ?? null, - 'city' => $payload['city'], - 'zipCode' => $payload['postCode'], - 'countryId' => $country->id, + 'addressLine1' => $payload['addressLine1'] ?? null, + 'addressLine2' => $payload['addressLine2'] ?? null, + 'locality' => $payload['city'], + 'postalCode' => $payload['postCode'], + 'countryCode' => 'NO', ]); - $order->setBillingAddress($address); - $order->setShippingAddress($address); + $order->setEstimatedShippingAddress($address); + $order->setEstimatedBillingAddress($address); - $methods = array_map(function(ShippingMethod $method) use ($order, &$isFirst, $currentHandle) { + $methods = array_map(function (ShippingMethod $method) use ($order, &$isFirst, $currentHandle) { $price = (string)$method->getPriceForOrder($order); $isDefault = 'N'; - if ((!$currentHandle && $isFirst) || $currentHandle === $method->getHandle()) { + if ($currentHandle === $method->getHandle() || $currentHandle === null && $isFirst) { $isDefault = 'Y'; + $isFirst = false; } return [ @@ -381,7 +383,7 @@ public function verifyAuthToken(Action $action) } $token = Craft::$app->getRequest()->getHeaders()->get('authorization'); - $gateway = Vipps::$plugin->payments->getGateway(); + $gateway = Vipps::$plugin->getPayments()->getGateway(); $authToken = $gateway->getAuthToken(); if (!$token || $authToken !== $token) { @@ -420,7 +422,7 @@ private function _saveTransaction($child) /** * Updates a transaction. * - * @param Transaction $transaction + * @param Transaction $transaction * @param RequestResponseInterface $response * * @throws TransactionException diff --git a/src/controllers/ExpressController.php b/src/controllers/ExpressController.php index 8533152..a323029 100644 --- a/src/controllers/ExpressController.php +++ b/src/controllers/ExpressController.php @@ -14,10 +14,15 @@ use craft\commerce\errors\PaymentException; use craft\commerce\models\Transaction; use craft\commerce\Plugin; +use craft\errors\ElementNotFoundException; use craft\web\Controller; use superbig\vipps\helpers\LogToFile; use superbig\vipps\Vipps; +use Throwable; +use yii\base\Exception; +use yii\web\BadRequestHttpException; +use function count; /** * @author Superbig @@ -30,11 +35,11 @@ class ExpressController extends Controller // ========================================================================= /** - * @var bool|array Allows anonymous access to this controller's actions. + * @var array|bool|int Allows anonymous access to this controller's actions. * The actions must be in 'kebab-case' * @access protected */ - protected $allowAnonymous = ['checkout']; + protected int|bool|array $allowAnonymous = ['checkout']; // Public Methods // ========================================================================= @@ -43,12 +48,12 @@ class ExpressController extends Controller * Initiate Express payment * * @return mixed - * @throws \Throwable - * @throws \craft\errors\ElementNotFoundException - * @throws \yii\base\Exception - * @throws \yii\web\BadRequestHttpException + * @throws Throwable + * @throws ElementNotFoundException + * @throws Exception + * @throws BadRequestHttpException */ - public function actionCheckout() + public function actionCheckout(): mixed { $request = Craft::$app->getRequest(); $commerce = Plugin::getInstance(); @@ -81,7 +86,7 @@ public function actionCheckout() // Ignore zero value qty for multi-add forms https://github.com/craftcms/commerce/issues/330#issuecomment-384533139 if ($qty > 0) { - $lineItem = $lineItemService->resolveLineItem($order->id, $purchasableId, $options); + $lineItem = $lineItemService->resolveLineItem($order, $purchasableId, $options); // New line items already have a qty of one. if ($lineItem->id) { @@ -101,7 +106,7 @@ public function actionCheckout() $originalTotalPrice = $order->getOutstandingBalance(); $originalTotalQty = $order->getTotalQty(); - $originalTotalAdjustments = \count($order->getAdjustments()); + $originalTotalAdjustments = count($order->getAdjustments()); // Do one final save to confirm the price does not change out from under the customer. Also removes any out of stock items etc. // This also confirms the products are available and discounts are current. @@ -111,7 +116,7 @@ public function actionCheckout() if (Craft::$app->getElements()->saveElement($order)) { $totalPriceChanged = $originalTotalPrice !== $order->getOutstandingBalance(); $totalQtyChanged = $originalTotalQty !== $order->getTotalQty(); - $totalAdjustmentsChanged = $originalTotalAdjustments !== \count($order->getAdjustments()); + $totalAdjustmentsChanged = $originalTotalAdjustments !== count($order->getAdjustments()); // Has the order changed in a significant way? if ($totalPriceChanged || $totalQtyChanged || $totalAdjustmentsChanged) { @@ -130,7 +135,7 @@ public function actionCheckout() $customError = Craft::t('commerce', 'Something changed with the order before payment, please review your order and submit payment again.'); if ($request->getAcceptsJson()) { - return $this->asErrorJson($customError); + return $this->asJson(['error' => $customError]); } $session->setError($customError); diff --git a/src/gateways/Gateway.php b/src/gateways/Gateway.php index 2488759..9b0ff8d 100644 --- a/src/gateways/Gateway.php +++ b/src/gateways/Gateway.php @@ -20,6 +20,7 @@ use craft\helpers\StringHelper; use superbig\vipps\Vipps; +use yii\base\Exception; /** * @author Superbig @@ -30,27 +31,24 @@ class Gateway extends BaseGateway { use GatewayTrait; - // Public Properties - // ========================================================================= - - public $clientId = ''; - public $clientSecret = ''; - public $subscriptionKeyAccessToken = ''; - public $subscriptionKeyEcommerce = ''; - public $merchantSerialNumber = ''; - public $transactionText = ''; - public $testMode = false; - public $expressCheckout = true; - public $createUserOnExpressCheckout = true; - public $loginWithVipps = false; - public $addItemToCartIfAlreadyExists = false; - public $newCartOnExpressCheckout = true; - public $fallbackUrl = ''; - public $errorFallbackUrl = ''; - public $authToken = ''; - public $captureOnStatusChange = false; - public $captureStatusUid = ''; - public $useBillingPhoneAsVippsPhoneNumber = true; + public string $clientId = ''; + public string $clientSecret = ''; + public string $subscriptionKeyAccessToken = ''; + public string $subscriptionKeyEcommerce = ''; + public string $merchantSerialNumber = ''; + public string $transactionText = ''; + public bool $testMode = false; + public bool $expressCheckout = true; + public bool $createUserOnExpressCheckout = true; + public bool $loginWithVipps = false; + public bool $addItemToCartIfAlreadyExists = false; + public bool $newCartOnExpressCheckout = true; + public string $fallbackUrl = ''; + public string $errorFallbackUrl = ''; + public string $authToken = ''; + public bool $captureOnStatusChange = false; + public string $captureStatusUid = ''; + public bool $useBillingPhoneAsVippsPhoneNumber = true; public static function displayName(): string @@ -58,9 +56,6 @@ public static function displayName(): string return Craft::t('vipps', 'Vipps'); } - // Public Methods - // ========================================================================= - /** * Makes an authorize request. * @@ -71,9 +66,7 @@ public static function displayName(): string */ public function authorize(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface { - $request = Vipps::$plugin->getPayments()->intiatePaymentFromGateway($transaction); - - return $request; + return Vipps::$plugin->getPayments()->intiatePaymentFromGateway($transaction); } /** @@ -83,39 +76,12 @@ public function authorize(Transaction $transaction, BasePaymentForm $form): Requ * @param string $reference Reference for the transaction being captured. * * @return RequestResponseInterface - * @throws \yii\base\Exception + * @throws Exception */ public function capture(Transaction $transaction, string $reference): RequestResponseInterface { // https://github.com/craftcms/commerce-omnipay/blob/master/src/base/Gateway.php#L549 - $response = Vipps::$plugin->getPayments()->captureFromGateway($transaction); - - return $response; - } - - /** - * Complete the purchase for offsite payments. - * - * @param Transaction $transaction The transaction - * - * @return RequestResponseInterface - */ - public function completePurchase(Transaction $transaction): RequestResponseInterface - { - // TODO: Implement completePurchase() method. - } - - /** - * Makes a purchase request. - * - * @param Transaction $transaction The purchase transaction - * @param BasePaymentForm $form A form filled with payment info - * - * @return RequestResponseInterface - */ - public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface - { - // TODO: Implement purchase() method. + return Vipps::$plugin->getPayments()->captureFromGateway($transaction); } /** @@ -124,13 +90,11 @@ public function purchase(Transaction $transaction, BasePaymentForm $form): Reque * @param Transaction $transaction The refund transaction * * @return RequestResponseInterface - * @throws \yii\base\Exception + * @throws Exception */ public function refund(Transaction $transaction): RequestResponseInterface { - $response = Vipps::$plugin->getPayments()->refundFromGateway($transaction); - - return $response; + return Vipps::$plugin->getPayments()->refundFromGateway($transaction); } public function getAuthToken() @@ -139,7 +103,7 @@ public function getAuthToken() } - public function getSettingsHtml() + public function getSettingsHtml(): ?string { return Craft::$app->getView()->renderTemplate('vipps/gatewaySettings', [ 'gateway' => $this, @@ -155,7 +119,7 @@ public function getPaymentTypeOptions(): array ]; } - public function getOrderStatuses() + public function getOrderStatuses(): array { return array_map(function(OrderStatus $orderStatus) { return [ @@ -165,7 +129,7 @@ public function getOrderStatuses() }, Plugin::getInstance()->getOrderStatuses()->getAllOrderStatuses()); } - public function rules() + public function rules(): array { return array_merge(parent::rules(), [ [ diff --git a/src/gateways/GatewayTrait.php b/src/gateways/GatewayTrait.php index 59f96a9..1f4319e 100644 --- a/src/gateways/GatewayTrait.php +++ b/src/gateways/GatewayTrait.php @@ -17,6 +17,7 @@ use craft\commerce\models\PaymentSource; use craft\commerce\models\Transaction; use craft\web\Response as WebResponse; +use Throwable; use yii\base\NotSupportedException; /** @@ -26,6 +27,31 @@ */ trait GatewayTrait { + /** + * Complete the purchase for offsite payments. + * + * @param Transaction $transaction The transaction + * + * @return RequestResponseInterface + */ + public function completePurchase(Transaction $transaction): RequestResponseInterface + { + // TODO: Implement completePurchase() method. + } + + /** + * Makes a purchase request. + * + * @param Transaction $transaction The purchase transaction + * @param BasePaymentForm $form A form filled with payment info + * + * @return RequestResponseInterface + */ + public function purchase(Transaction $transaction, BasePaymentForm $form): RequestResponseInterface + { + // TODO: Implement purchase() method. + } + /** * Returns payment Form HTML * @@ -33,7 +59,7 @@ trait GatewayTrait * * @return string|null */ - public function getPaymentFormHtml(array $params) + public function getPaymentFormHtml(array $params): ?string { return ''; } @@ -46,9 +72,8 @@ public function getPaymentFormHtml(array $params) * * @return PaymentSource */ - public function createPaymentSource(BasePaymentForm $sourceData, int $userId): PaymentSource + public function createPaymentSource(BasePaymentForm $sourceData, int $customerId): PaymentSource { - return false; } /** @@ -58,7 +83,7 @@ public function createPaymentSource(BasePaymentForm $sourceData, int $userId): P * * @return bool */ - public function deletePaymentSource($token): bool + public function deletePaymentSource(string $token): bool { return false; } @@ -67,7 +92,7 @@ public function deletePaymentSource($token): bool * Processes a webhook and return a response * * @return WebResponse - * @throws \Throwable if something goes wrong + * @throws Throwable if something goes wrong */ public function processWebHook(): WebResponse { diff --git a/src/helpers/Currency.php b/src/helpers/Currency.php index 12b82af..abe1377 100644 --- a/src/helpers/Currency.php +++ b/src/helpers/Currency.php @@ -2,27 +2,29 @@ namespace superbig\vipps\helpers; +use function round; + class Currency { /** * Round amount to ensure enough digits * * @param float $amount - * @param int $precision + * @param int $precision * * @return false|float */ - public static function round($amount, $precision = 2) + public static function round(float $amount, int $precision = 2): float|bool { - return \round($amount, $precision); + return round($amount, $precision); } - public static function roundAndConvertToMinorUnit($amount) + public static function roundAndConvertToMinorUnit($amount): int { return intval(round(floatval("{$amount}") * 100, 2)); } - public static function convertFromMinorUnit($amount) + public static function convertFromMinorUnit($amount): float|int { return $amount / 100; } diff --git a/src/helpers/LogToFile.php b/src/helpers/LogToFile.php index c2dc5d1..cdb8878 100644 --- a/src/helpers/LogToFile.php +++ b/src/helpers/LogToFile.php @@ -33,26 +33,26 @@ class LogToFile /** * @var string */ - public static $handle = 'vipps'; + public static string $handle = 'vipps'; /** * @var bool */ - public static $logToCraft = true; + public static bool $logToCraft = true; /** * @var bool * @deprecated in 1.1.0 */ - public static $logUserIp = false; + public static bool $logUserIp = false; /** * Logs an info message to a file with the provided handle. * - * @param string|array $message + * @param array|string $message * @param string|null $handle */ - public static function info($message, string $handle = null) + public static function info(array|string $message, string $handle = null): void { self::log($message, $handle, 'info'); } @@ -60,10 +60,10 @@ public static function info($message, string $handle = null) /** * Logs an error message to a file with the provided handle. * - * @param string|array $message + * @param array|string $message * @param string|null $handle */ - public static function error($message, string $handle = null) + public static function error(array|string $message, string $handle = null): void { self::log($message, $handle, 'error'); } @@ -71,11 +71,11 @@ public static function error($message, string $handle = null) /** * Logs the message to a file with the provided handle and level. * - * @param string|array $message + * @param array|string $message * @param string|null $handle * @param string $level */ - public static function log($message, string $handle = null, string $level = 'info') + public static function log(array|string $message, string $handle = null, string $level = 'info'): void { $ip = ''; $userId = ''; @@ -127,25 +127,23 @@ public static function log($message, string $handle = null, string $level = 'inf } } - public static function encodeForLog($data) + public static function encodeForLog($data): string { return Json::encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } - public static function formatLogVariables($variables = []) + public static function formatLogVariables($variables = []): array { - $variables = array_map(function($value) { + return array_map(function($value) { if (is_array($value)) { $value = Json::encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } return $value; }, $variables); - - return $variables; } - public static function formatLogMessage($message, $variables = []) + public static function formatLogMessage($message, $variables = []): string { if (is_array($message)) { $message = Json::encode($message, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); diff --git a/src/helpers/StringHelper.php b/src/helpers/StringHelper.php index 07ef6aa..b6df77c 100644 --- a/src/helpers/StringHelper.php +++ b/src/helpers/StringHelper.php @@ -46,4 +46,23 @@ public static function transactionId(): string random_int(0, 0xffff) ); } + + /** + * Make sure a phone number is 8 chars and isn't prefixed with +xx + * + * @param string $phone + * @return string + */ + public static function getCleanPhone(string $phone) + { + // Strip space from phone number + $phone = str_replace(' ', '', $phone); + + // Strip the prefix if it starts with +xx or 00xx + if (preg_match('/^(\+\d{2}|00\d{2})\d{8}$/', $phone, $matches)) { + $phone = substr($phone, strlen($matches[1])); + } + + return $phone; + } } diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 228235e..0a83c9b 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -23,19 +23,14 @@ */ class Install extends Migration { - // Public Properties - // ========================================================================= - + /** * @var string The database driver to use */ - public $driver; - - // Public Methods - // ========================================================================= + public string $driver; - public function safeUp() + public function safeUp(): bool { $this->driver = Craft::$app->getConfig()->getDb()->driver; if ($this->createTables()) { @@ -50,7 +45,7 @@ public function safeUp() } - public function safeDown() + public function safeDown(): bool { $this->driver = Craft::$app->getConfig()->getDb()->driver; $this->removeTables(); @@ -58,13 +53,10 @@ public function safeDown() return true; } - // Protected Methods - // ========================================================================= - /** * @return bool */ - protected function createTables() + protected function createTables(): bool { $tablesCreated = false; @@ -91,25 +83,17 @@ protected function createTables() /** * @return void */ - protected function createIndexes() + protected function createIndexes(): void { $this->createIndex( - $this->db->getIndexName( - PaymentRecord::tableName(), - 'shortId', - true - ), + $this->db->getIndexName(), PaymentRecord::tableName(), 'shortId', true ); $this->createIndex( - $this->db->getIndexName( - PaymentRecord::tableName(), - 'transactionReference', - true - ), + $this->db->getIndexName(), PaymentRecord::tableName(), 'transactionReference', true @@ -119,10 +103,10 @@ protected function createIndexes() /** * @return void */ - protected function addForeignKeys() + protected function addForeignKeys(): void { $this->addForeignKey( - $this->db->getForeignKeyName(PaymentRecord::tableName(), 'orderId'), + $this->db->getForeignKeyName(), PaymentRecord::tableName(), 'orderId', '{{%elements}}', @@ -135,7 +119,7 @@ protected function addForeignKeys() /** * @return void */ - protected function removeTables() + protected function removeTables(): void { $this->dropTableIfExists(PaymentRecord::tableName()); } diff --git a/src/models/ErrorModel.php b/src/models/ErrorModel.php index 7c98186..478f1cc 100644 --- a/src/models/ErrorModel.php +++ b/src/models/ErrorModel.php @@ -21,46 +21,44 @@ */ class ErrorModel extends Model { - // Public Properties - // ========================================================================= - + /** * @var string */ - public $errorCode = ''; + public string $errorCode = ''; /** * @var string */ - public $errorMessage = ''; + public string $errorMessage = ''; /** * @var string */ - public $errorGroup = ''; + public string $errorGroup = ''; /** * @var int */ - public $orderId; + public int $orderId; // Public Methods // ========================================================================= - public function getErrorGroupLabel() + public function getErrorGroupLabel(): void { $groups = [ 'Authentication' => 'Authentication', // Authentication Failure because of wrong token provided 'Payment' => 'Payment', // Failure while doing a payment Authorization, mostly because of PSP errors 'InvalidRequest' => 'InvalidRequest', // Request contains invalid parameters 'VippsError' => 'VippsError', // Internal Vipps application error - 'Customer' => 'Customer', // Error raised because of Vipps user (Example: User not registered with Vipps .... + 'Customer' => 'Customer', // Error raised because of Vipps user (Example: User not registered with Vipps ....) 'Merchant' => 'Merchant', // Errors regarding the merchant ]; } - public function rules() + public function rules(): array { return [ [['shortId', 'orderId'], 'required'], diff --git a/src/models/PaymentModel.php b/src/models/PaymentModel.php index c4f7a5e..8568bf9 100644 --- a/src/models/PaymentModel.php +++ b/src/models/PaymentModel.php @@ -21,29 +21,27 @@ */ class PaymentModel extends Model { - // Public Properties - // ========================================================================= - + /** * @var string */ - public $shortId = ''; + public string $shortId = ''; /** * @var int */ - public $orderId; + public int $orderId; /** * @var int */ - public $transactionReference; + public int $transactionReference; // Public Methods // ========================================================================= - public function rules() + public function rules(): array { return [ [['shortId', 'orderId', 'transactionReference'], 'required'], diff --git a/src/models/PaymentRequestModel.php b/src/models/PaymentRequestModel.php index d2b6609..8cb898a 100644 --- a/src/models/PaymentRequestModel.php +++ b/src/models/PaymentRequestModel.php @@ -15,6 +15,7 @@ use craft\commerce\elements\Order; use craft\commerce\models\Settings; use craft\commerce\models\Transaction; +use craft\helpers\App; use craft\helpers\UrlHelper; use DateTime; @@ -26,15 +27,13 @@ * @package Vipps * @since 1.0.0 * - * @property string $mobileNumber The account to send Vipps request to - * @property Order $order + * @property string $mobileNumber The account to send Vipps request to + * @property Order $order * @property Transaction $transaction - * @property float $amount Total amount in minor units (øre) + * @property float $amount Total amount in minor units (øre) */ class PaymentRequestModel extends Model { - // Public Properties - // ========================================================================= const TYPE_EXPRESS = 'express'; const TYPE_REGULAR = 'regular'; @@ -45,31 +44,31 @@ class PaymentRequestModel extends Model self::TYPE_REGULAR => 'eComm Regular Payment', ]; - public $mobileNumber = ''; - public $type = self::TYPE_EXPRESS; - public $amount; - public $orderId; - public $order; - public $transaction; - private $transactionShortId; - private $_transactionText; - private $_url; + public string $mobileNumber = ''; + public string $type = self::TYPE_EXPRESS; + public float|int $amount = 0; + public string $orderId; + public ?Order $order; + public ?Transaction $transaction; + private string $transactionShortId; + private string $_transactionText = ''; + private string $_url = ''; // Public Methods // ========================================================================= - public function init() + public function init(): void { parent::init(); $this->transactionShortId = StringHelper::transactionId(); } - public function getPayload() + public function getPayload(): array { // Settings $callbackPrefix = UrlHelper::siteUrl('vipps/callbacks'); - $timestamp = (new \DateTime())->format(DateTime::ATOM); + $timestamp = (new DateTime())->format(DateTime::ATOM); // Order info $orderId = $this->order->id; @@ -97,7 +96,7 @@ public function getPayload() 'consentRemovalPrefix' => $callbackPrefix, 'fallBack' => $fallbackUrl, 'isApp' => false, - 'merchantSerialNumber' => Craft::parseEnv($gateway->merchantSerialNumber), + 'merchantSerialNumber' => App::parseEnv($gateway->merchantSerialNumber), 'paymentType' => $this->getType(), ], 'transaction' => @@ -111,7 +110,7 @@ public function getPayload() if ($gateway->useBillingPhoneAsVippsPhoneNumber && !empty($billingAddress->phone)) { $payload['customerInfo'] = [ - 'mobileNumber' => $billingAddress->phone, + 'mobileNumber' => StringHelper::getCleanPhone($billingAddress->phone), ]; } @@ -120,8 +119,8 @@ public function getPayload() public function getTransactionText(): string { - if (!$this->_transactionText) { - $this->_transactionText = Vipps::$plugin->payments->getTransactionText($this->order); + if (empty($this->_transactionText)) { + $this->_transactionText = Vipps::$plugin->getPayments()->getTransactionText($this->order); } return $this->_transactionText; @@ -142,7 +141,7 @@ public function getPaymentRecord(): PaymentModel ]); } - public function setType($type) + public function setType($type): static { $this->type = $type; @@ -151,15 +150,15 @@ public function setType($type) public function getType(): string { - return self::PAYMENT_TYPE_PARAMS[ $this->type ]; + return self::PAYMENT_TYPE_PARAMS[$this->type]; } - public function getUrl() + public function getUrl(): string { return $this->_url; } - public function setUrl($url = null) + public function setUrl($url = null): static { $this->_url = $url; @@ -167,7 +166,7 @@ public function setUrl($url = null) } - public function rules() + public function rules(): array { return [ [['order'], 'required'], diff --git a/src/models/Settings.php b/src/models/Settings.php deleted file mode 100644 index 71854ba..0000000 --- a/src/models/Settings.php +++ /dev/null @@ -1,55 +0,0 @@ - 'Order #{number}'], - ]; - } -} diff --git a/src/records/PaymentRecord.php b/src/records/PaymentRecord.php index 5a7b0cf..a41d674 100644 --- a/src/records/PaymentRecord.php +++ b/src/records/PaymentRecord.php @@ -21,11 +21,7 @@ */ class PaymentRecord extends ActiveRecord { - // Public Static Methods - // ========================================================================= - - - public static function tableName() + public static function tableName(): string { return '{{%vipps_payments}}'; } diff --git a/src/responses/CallbackResponse.php b/src/responses/CallbackResponse.php index d809e7e..eeb9d2b 100644 --- a/src/responses/CallbackResponse.php +++ b/src/responses/CallbackResponse.php @@ -12,7 +12,9 @@ use craft\commerce\base\RequestResponseInterface; use craft\helpers\ArrayHelper; +use Exception; use superbig\vipps\Vipps; +use function in_array; /** * @author Superbig @@ -29,18 +31,9 @@ class CallbackResponse implements RequestResponseInterface const STATUS_CANCELLED = 'CANCELLED'; const STATUS_REJECTED = 'REJECTED'; - /** - * @var - */ - protected $data = []; - /** - * @var string - */ - private $_redirect = ''; - /** - * @var bool - */ - private $_processing = false; + protected array $data = []; + private string $_redirect = ''; + private bool $_processing = false; /** * Response constructor. @@ -52,25 +45,23 @@ public function __construct($data) $this->data = $data; } - // Public Properties - // ========================================================================= - public function setRedirectUrl(string $url) + public function setRedirectUrl(string $url): void { $this->_redirect = $url; } - public function setProcessing(bool $status) + public function setProcessing(bool $status): void { $this->_processing = $status; } /** - * Returns whether or not the payment was successful. + * Returns whether the payment was successful. * * @return bool - * @throws \Exception + * @throws Exception */ public function isSuccessful(): bool { @@ -78,7 +69,7 @@ public function isSuccessful(): bool $status = ArrayHelper::getValue($this->data, 'transactionInfo.status'); return !$error && - \in_array($status, [ + in_array($status, [ self::STATUS_RESERVE, self::STATUS_RESERVED, self::STATUS_SALE, @@ -86,7 +77,7 @@ public function isSuccessful(): bool } /** - * Returns whether or not the payment is being processed by gateway. + * Returns whether the payment is being processed by gateway. * * @return bool */ @@ -102,7 +93,7 @@ public function isRedirect(): bool } /** - * Returns whether or not this is a Express order + * Returns whether this is an Express order * * @return bool */ @@ -167,7 +158,7 @@ public function getCode(): string * * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } @@ -187,7 +178,7 @@ public function getMessage(): string * * @return string */ - public function getEmail(): string + public function getEmail(): string|null { return $this->data['userDetails']['email'] ?? null; } @@ -199,7 +190,7 @@ public function getEmail(): string * * @return int */ - public function getAmount($convert = true): int + public function getAmount(bool $convert = true): int { $amount = ArrayHelper::getValue($this->data, 'transactionInfo.amount', 0); @@ -216,8 +207,8 @@ public function getAmount($convert = true): int * * @return mixed */ - public function redirect() + public function redirect(): void { - return false; + return; } } diff --git a/src/responses/CaptureResponse.php b/src/responses/CaptureResponse.php index a7785f0..a84ee3e 100644 --- a/src/responses/CaptureResponse.php +++ b/src/responses/CaptureResponse.php @@ -11,7 +11,6 @@ namespace superbig\vipps\responses; use craft\commerce\base\RequestResponseInterface; -use superbig\vipps\Vipps; /** * @author Superbig @@ -23,15 +22,15 @@ class CaptureResponse implements RequestResponseInterface /** * @var */ - protected $data = []; + protected array $data = []; /** * @var string */ - private $_redirect = ''; + private string $_redirect = ''; /** * @var bool */ - private $_processing = false; + private bool $_processing = false; /** * Response constructor. @@ -43,22 +42,20 @@ public function __construct($data) $this->data = $data; } - // Public Properties - // ========================================================================= - - public function setRedirectUrl(string $url) + + public function setRedirectUrl(string $url): void { $this->_redirect = $url; } - public function setProcessing(bool $status) + public function setProcessing(bool $status): void { $this->_processing = $status; } /** - * Returns whether or not the payment was successful. + * Returns whether the payment was successful. * * @return bool */ @@ -68,7 +65,7 @@ public function isSuccessful(): bool } /** - * Returns whether or not the payment is being processed by gateway. + * Returns whether the payment is being processed by gateway. * * @return bool */ @@ -143,7 +140,7 @@ public function getCode(): string * * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } @@ -165,10 +162,10 @@ public function getMessage(): string /** * Perform the redirect. * - * @return mixed + * @return void */ - public function redirect() + public function redirect(): void { - return false; + return; } } diff --git a/src/responses/ErrorResponse.php b/src/responses/ErrorResponse.php index 7de41f9..f7cd8be 100644 --- a/src/responses/ErrorResponse.php +++ b/src/responses/ErrorResponse.php @@ -27,11 +27,11 @@ */ class ErrorResponse implements RequestResponseInterface { - protected $exception; - private $orderId = ''; - protected $data = []; - private $_redirect = ''; - private $_processing = false; + protected BadResponseException $exception; + private mixed $orderId = ''; + protected mixed $data = []; + private string $_redirect = ''; + private bool $_processing = false; /** * Response constructor. @@ -45,22 +45,20 @@ public function __construct(BadResponseException $exception, $orderId = '') $this->data = $exception->hasResponse() ? Json::decodeIfJson((string)$exception->getResponse()->getBody()) : []; } - // Public Properties - // ========================================================================= - - public function setRedirectUrl(string $url) + + public function setRedirectUrl(string $url): void { $this->_redirect = $url; } - public function setProcessing(bool $status) + public function setProcessing(bool $status): void { $this->_processing = $status; } /** - * Returns whether or not the payment was successful. + * Returns whether the payment was successful. * * @return bool */ @@ -70,7 +68,7 @@ public function isSuccessful(): bool } /** - * Returns whether or not the payment is being processed by gateway. + * Returns whether the payment is being processed by gateway. * * @return bool */ @@ -137,7 +135,7 @@ public function getCode(): string * * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } @@ -157,8 +155,8 @@ public function getMessage(): string * * @return mixed */ - public function redirect() + public function redirect(): void { - return false; + return; } } diff --git a/src/responses/PaymentResponse.php b/src/responses/PaymentResponse.php index 6874630..973348b 100644 --- a/src/responses/PaymentResponse.php +++ b/src/responses/PaymentResponse.php @@ -25,18 +25,18 @@ class PaymentResponse implements RequestResponseInterface /** * @var */ - protected $data = []; + protected array $data = []; /** * @var string */ - private $_redirect = ''; + private string $_redirect = ''; /** * @var bool */ - private $_processing = false; + private bool $_processing = false; - private $_error; - private $_code = 200; + private null|string $_error = null; + private mixed $_code = 200; /** * Response constructor. @@ -62,22 +62,20 @@ public function __construct($data) } } - // Public Properties - // ========================================================================= - - public function setRedirectUrl(string $url) + + public function setRedirectUrl(string $url): void { $this->_redirect = $url; } - public function setProcessing(bool $status) + public function setProcessing(bool $status): void { $this->_processing = $status; } /** - * Returns whether or not the payment was successful. + * Returns whether the payment was successful. * * @return bool */ @@ -91,7 +89,7 @@ public function isSuccessful(): bool } /** - * Returns whether or not the payment is being processed by gateway. + * Returns whether the payment is being processed by gateway. * * @return bool */ @@ -162,7 +160,7 @@ public function getCode(): string * * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } @@ -186,8 +184,10 @@ public function getMessage(): string * * @return mixed */ - public function redirect() + public function redirect(): void { - return Craft::$app->getResponse()->redirect($this->_redirect)->send(); + Craft::$app->getResponse()->redirect($this->_redirect)->send(); + + return; } } diff --git a/src/responses/RefundResponse.php b/src/responses/RefundResponse.php index 3d6bfd1..2927532 100644 --- a/src/responses/RefundResponse.php +++ b/src/responses/RefundResponse.php @@ -25,15 +25,15 @@ class RefundResponse implements RequestResponseInterface /** * @var */ - protected $data = []; + protected array $data = []; /** * @var string */ - private $_redirect = ''; + private string $_redirect = ''; /** * @var bool */ - private $_processing = false; + private bool $_processing = false; /** * Response constructor. @@ -45,22 +45,20 @@ public function __construct($data) $this->data = $data; } - // Public Properties - // ========================================================================= - - public function setRedirectUrl(string $url) + + public function setRedirectUrl(string $url): void { $this->_redirect = $url; } - public function setProcessing(bool $status) + public function setProcessing(bool $status): void { $this->_processing = $status; } /** - * Returns whether or not the payment was successful. + * Returns whether the payment was successful. * * @return bool */ @@ -70,7 +68,7 @@ public function isSuccessful(): bool } /** - * Returns whether or not the payment is being processed by gateway. + * Returns whether the payment is being processed by gateway. * * @return bool */ @@ -141,7 +139,7 @@ public function getCode(): string * * @return mixed */ - public function getData() + public function getData(): mixed { return $this->data; } @@ -161,8 +159,10 @@ public function getMessage(): string * * @return mixed */ - public function redirect() + public function redirect(): void { - return Craft::$app->getResponse()->redirect($this->_redirect)->send(); + Craft::$app->getResponse()->redirect($this->_redirect)->send(); + + return; } } diff --git a/src/services/Api.php b/src/services/Api.php index c1c9a6a..fcc647c 100644 --- a/src/services/Api.php +++ b/src/services/Api.php @@ -13,7 +13,9 @@ use Craft; use craft\base\Component; use craft\commerce\Plugin as CommercePlugin; +use craft\helpers\App; use craft\helpers\Json; +use Exception; use GuzzleHttp\Client; use GuzzleHttp\Exception\BadResponseException; use GuzzleHttp\Psr7\Query; @@ -31,10 +33,10 @@ class Api extends Component const ENDPOINT = 'https://api.vipps.no'; const TEST_ENDPOINT = 'https://apitest.vipps.no'; - private $_client; - private $_accessToken; + private ?Client $_client = null; + private string|null $_accessToken = null; - public function init() + public function init(): void { // Set initial token $this->_getAccessToken(); @@ -43,7 +45,7 @@ public function init() /** * @return array|null */ - public function getAccessTokenHeader() + public function getAccessTokenHeader(): ?array { $token = $this->_accessToken; @@ -55,7 +57,7 @@ public function getAccessTokenHeader() return [ 'Authorization' => 'Bearer ' . $token, - 'ocp-apim-subscription-key' => Craft::parseEnv($gateway->subscriptionKeyAccessToken), + 'ocp-apim-subscription-key' => App::parseEnv($gateway->subscriptionKeyAccessToken), ]; } @@ -71,11 +73,11 @@ public function getApiUrl(): string /** * @param string $url - * @param array $query + * @param array $query * * @return array|null */ - public function get($url = '', $query = []) + public function get(string $url = '', array $query = []): ?array { try { $client = $this->getClient(); @@ -84,17 +86,14 @@ public function get($url = '', $query = []) 'query' => Query::build($query), ]); $body = (string)$response->getBody(); - $json = Json::decodeIfJson($body); - - - return $json; + return Json::decodeIfJson($body); } catch (BadResponseException $e) { $responseBody = (string)$e->getResponse()->getBody(); $json = Json::decodeIfJson($responseBody); $this->_logException($e); return $json; - } catch (\Exception $e) { + } catch (Exception $e) { $this->_logException($e); return null; @@ -103,11 +102,11 @@ public function get($url = '', $query = []) /** * @param string $url - * @param array $data + * @param array $data * * @return array|null */ - public function post($url = '', $data = []) + public function post(string $url = '', array $data = []): ?array { try { $client = $this->getClient(); @@ -116,9 +115,7 @@ public function post($url = '', $data = []) 'json' => $data, ]); $body = (string)$response->getBody(); - $json = Json::decodeIfJson($body); - - return $json; + return Json::decodeIfJson($body); } catch (BadResponseException $e) { $responseBody = (string)$e->getResponse()->getBody(); $json = Json::decodeIfJson($responseBody); @@ -126,7 +123,7 @@ public function post($url = '', $data = []) $this->_logException($e); return $json; - } catch (\Exception $e) { + } catch (Exception $e) { $this->_logException($e); return null; @@ -164,7 +161,7 @@ private function _getAccessToken() return $this->_accessToken; } - private function _logException(\Exception $e) + private function _logException(Exception $e): void { if ($e instanceof BadResponseException) { $url = $e->getRequest()->getUri(); @@ -213,10 +210,10 @@ private function _getDefaultHeaders(): array 'X-TimeStamp' => $date, 'X-Source-Address' => $ip, 'cache-control' => 'no-cache', - 'ocp-apim-subscription-key' => Craft::parseEnv($gateway->subscriptionKeyAccessToken), - 'client_id' => Craft::parseEnv($gateway->clientId), - 'client_secret' => Craft::parseEnv($gateway->clientSecret), - 'Merchant-Serial-Number' => Craft::parseEnv($gateway->merchantSerialNumber), + 'ocp-apim-subscription-key' => App::parseEnv($gateway->subscriptionKeyAccessToken), + 'client_id' => App::parseEnv($gateway->clientId), + 'client_secret' => App::parseEnv($gateway->clientSecret), + 'Merchant-Serial-Number' => App::parseEnv($gateway->merchantSerialNumber), 'Vipps-System-Name' => 'craft-commerce', 'Vipps-System-Version' => CommercePlugin::getInstance()->getVersion(), 'Vipps-System-Plugin-Name' => 'craft-vipps', diff --git a/src/services/Express.php b/src/services/Express.php index b2f8e88..0d03992 100644 --- a/src/services/Express.php +++ b/src/services/Express.php @@ -18,6 +18,11 @@ use craft\helpers\UrlHelper; use superbig\vipps\Vipps; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; +use yii\base\Exception; +use function is_numeric; /** * @author Superbig @@ -46,10 +51,10 @@ public function onRegisterOrderAdjusters(RegisterComponentTypesEvent $e): void /** * @param null $purchasable * - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * @throws \yii\base\Exception + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + * @throws Exception */ public function getButton($purchasable = null, array $config = []): string { @@ -57,7 +62,7 @@ public function getButton($purchasable = null, array $config = []): string $oldMode = $view->getTemplateMode(); $view->setTemplateMode($view::TEMPLATE_MODE_CP); - if (\is_numeric($purchasable)) { + if (is_numeric($purchasable)) { $purchasable = Plugin::getInstance()->getVariants()->getVariantById($purchasable); } @@ -101,10 +106,10 @@ public function getCheckoutUrl($purchasable = null, array $config = []): string /** * @param null $purchasable * - * @throws \Twig\Error\LoaderError - * @throws \Twig\Error\RuntimeError - * @throws \Twig\Error\SyntaxError - * @throws \yii\base\Exception + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError + * @throws Exception */ public function getFormButton($purchasable = null, array $config = []): string { diff --git a/src/services/Payments.php b/src/services/Payments.php index a38e88e..f6bd23e 100644 --- a/src/services/Payments.php +++ b/src/services/Payments.php @@ -20,6 +20,7 @@ use craft\commerce\Plugin; use craft\commerce\records\Transaction as TransactionRecord; use craft\db\Query; +use craft\helpers\App; use craft\helpers\ArrayHelper; use craft\helpers\UrlHelper; use superbig\vipps\gateways\Gateway; @@ -44,7 +45,7 @@ class Payments extends Component private ?bool $_express = null; /** @var mixed|null */ - private $_gateway; + private mixed $_gateway = null; public function init(): void { @@ -53,7 +54,7 @@ public function init(): void /** * @param string|null $shortId */ - public function getOrderByShortId(string $shortId = null): ?\craft\commerce\elements\Order + public function getOrderByShortId(string $shortId = null): ?Order { $orderId = (new Query()) ->from(PaymentRecord::tableName()) @@ -68,11 +69,11 @@ public function getOrderByShortId(string $shortId = null): ?\craft\commerce\elem return Plugin::getInstance()->getOrders()->getOrderById($orderId); } - public function getTransactionByShortId(string $shortId = null): ?\craft\commerce\models\Transaction + public function getTransactionByShortId(string $shortId = null): ?Transaction { $reference = (new Query()) ->from(PaymentRecord::tableName()) - ->select('transactionReference') + ->select('shortId') ->where('shortId = :shortId', [':shortId' => $shortId]) ->scalar(); @@ -142,7 +143,7 @@ public function captureFromGateway(Transaction $transaction): CaptureResponse $amount = 0; $response = Vipps::$plugin->getApi()->post(sprintf('/ecomm/v2/payments/%s/capture', $parentTransaction->reference), [ 'merchantInfo' => [ - 'merchantSerialNumber' => Craft::parseEnv($gateway->merchantSerialNumber), + 'merchantSerialNumber' => App::parseEnv($gateway->merchantSerialNumber), ], 'transaction' => [ 'amount' => $amount, @@ -168,7 +169,7 @@ public function refundFromGateway(Transaction $transaction): CaptureResponse //dd($parentTransaction->reference, $amount, $transactionText); $response = Vipps::$plugin->getApi()->post(sprintf('/ecomm/v2/payments/%s/refund', $parentTransaction->reference), [ 'merchantInfo' => [ - 'merchantSerialNumber' => Craft::parseEnv($gateway->merchantSerialNumber), + 'merchantSerialNumber' => App::parseEnv($gateway->merchantSerialNumber), ], 'transaction' => [ 'amount' => $amount, @@ -186,7 +187,7 @@ public function onStatusChange(OrderStatusEvent $e): void try { $order = $e->order; $gateway = $this->getGateway(); - $enabled = $gateway && $gateway->captureOnStatusChange && $this->isVippsGateway($order); + $enabled = $gateway->captureOnStatusChange && $this->isVippsGateway($order); if ($enabled && $gateway->captureStatusUid === $e->orderHistory->getNewStatus()->uid) { $transaction = $this->getSuccessfulTransactionForOrder($order); @@ -224,7 +225,8 @@ public function createResponseFromCallback($payload = []): void public function getTransactionText(Order $order): string { - $text = Craft::parseEnv($this->getGateway()->transactionText); + $text = empty($this->getGateway()->transactionText) ? '{lineItemsText}' : $this->getGateway()->transactionText; + $text = App::parseEnv($text); return Craft::$app->getView()->renderObjectTemplate($text, $order, [ 'lineItemsText' => $this->getLineItemsAsText($order), @@ -235,7 +237,7 @@ public function getLineItemsAsText(Order $order): string { // @todo Crop? $lineItems = $order->getLineItems(); - $lines = array_map(static function (LineItem $item) : string { + $lines = array_map(static function (LineItem $item): string { $variant = $item->getPurchasable(); /** @var Variant $variant */ return sprintf('%dx %s', $item->qty, $variant->title); @@ -252,7 +254,7 @@ public function getFallbackActionUrl(string $transactionId): string public function getFallbackUrl(Order $order): string { $gateway = $this->getGateway(); - $url = Craft::parseEnv($gateway->fallbackUrl); + $url = App::parseEnv($gateway->fallbackUrl); $parsedUrl = Craft::$app->getView()->renderObjectTemplate($url, $order); $defaultUrl = UrlHelper::siteUrl('/'); @@ -262,7 +264,7 @@ public function getFallbackUrl(Order $order): string public function getFallbackErrorUrl(Order $order): string { $gateway = $this->getGateway(); - $url = Craft::parseEnv($gateway->errorFallbackUrl); + $url = App::parseEnv($gateway->errorFallbackUrl); $parsedUrl = Craft::$app->getView()->renderObjectTemplate($url, $order); $defaultUrl = $this->getFallbackUrl($order); @@ -283,7 +285,7 @@ public function getGateway(): Gateway return $this->_gateway; } - public function getSuccessfulTransactionForOrder(Order $order): ?\craft\commerce\models\Transaction + public function getSuccessfulTransactionForOrder(Order $order): ?Transaction { return ArrayHelper::firstWhere($order->getTransactions(), static fn(Transaction $transaction): bool => $transaction->status === TransactionRecord::STATUS_SUCCESS && $transaction->type === TransactionRecord::TYPE_AUTHORIZE @@ -308,8 +310,6 @@ public function getOrderDetails(Order $order): void public function isVippsGateway(Order $order): bool { - $orderGateway = $order->getGateway(); - - return $orderGateway !== null && $orderGateway instanceof Gateway; + return $order->getGateway() instanceof Gateway; } } diff --git a/src/templates/gatewaySettings.twig b/src/templates/gatewaySettings.twig index 1a53abf..f2f80c0 100644 --- a/src/templates/gatewaySettings.twig +++ b/src/templates/gatewaySettings.twig @@ -1,4 +1,4 @@ -{% from "_includes/forms" import textField, lightswitchField, selectField, autoSuggestField %} +{% from "_includes/forms" import textField, lightswitchField, selectField, autoSuggestField, selectizeField, booleanMenuField %} {{ autoSuggestField({ label: "Client Id"|t('vipps'), @@ -94,38 +94,48 @@ }) }} -{{ lightswitchField({ +{{ booleanMenuField({ label: "Test mode"|t('vipps'), name: 'testMode', - on: gateway.testMode, + value: gateway.testMode, + yesLabel: 'On'|t('app'), + noLabel: 'Off'|t('app'), errors: gateway.getErrors('testMode'), + includeEnvVars: true, }) }} -{{ lightswitchField({ +{{ booleanMenuField({ label: "Use billing phone number as Vipps phone number"|t('vipps'), description: "This will set the Vipps phone number to be the same as the billing address phone number, if any."|t('vipps'), name: 'useBillingPhoneAsVippsPhoneNumber', - on: gateway.useBillingPhoneAsVippsPhoneNumber, + yesLabel: 'Yes'|t('app'), + noLabel: 'No'|t('app'), + value: gateway.useBillingPhoneAsVippsPhoneNumber, errors: gateway.getErrors('useBillingPhoneAsVippsPhoneNumber'), + includeEnvVars: true, }) }} -{{ lightswitchField({ +{{ booleanMenuField({ label: "Capture on status change"|t('vipps'), description: "This will enable the gateway to automatically capture on a status change - say when you change from _New_ to _Shipped_"|t('vipps'), name: 'captureOnStatusChange', - on: gateway.captureOnStatusChange, + yesLabel: 'Yes'|t('app'), + noLabel: 'No'|t('app'), + value: gateway.captureOnStatusChange, errors: gateway.getErrors('captureOnStatusChange'), + includeEnvVars: true, }) }} -{{ selectField({ +{{ selectizeField({ label: "Capture status"|t("vipps"), instructions: "Status for automatic capture."|t("vipps"), id: 'captureStatusUid', name: 'captureStatusUid', value: gateway.captureStatusUid, options: statuses, - errors: gateway.getErrors('captureStatusUid') + errors: gateway.getErrors('captureStatusUid'), + includeEnvVars: true, }) }} {# @@ -139,10 +149,12 @@ #} -{{ lightswitchField({ - label: "New cart on Express Checkout"|t('vipps'), +{{ booleanMenuField({ + label: "Create new cart on Express Checkout"|t('vipps'), instructions: "This will clear the cart if there is already any items in it when a user performs a Express Checkout", name: 'newCartOnExpressCheckout', - on: gateway.newCartOnExpressCheckout, + value: gateway.newCartOnExpressCheckout, errors: gateway.getErrors('newCartOnExpressCheckout'), + yesLabel: 'Yes'|t('app'), + noLabel: 'No'|t('app'), }) }} diff --git a/src/templates/settings.twig b/src/templates/settings.twig deleted file mode 100644 index 3371c6b..0000000 --- a/src/templates/settings.twig +++ /dev/null @@ -1,16 +0,0 @@ -{# @var craft \craft\web\twig\variables\CraftVariable #} -{# -/** - * Vipps plugin for Craft CMS 3.x - * - * Vipps Settings.twig - * - * @author Superbig - * @copyright Copyright (c) 2018 Superbig - * @link https://superbig.co - * @package Vipps - * @since 1.0.0 - */ -#} - -{% import "_includes/forms" as forms %} \ No newline at end of file diff --git a/src/variables/VippsVariable.php b/src/variables/VippsVariable.php index f189d55..90d179c 100644 --- a/src/variables/VippsVariable.php +++ b/src/variables/VippsVariable.php @@ -14,6 +14,7 @@ use craft\helpers\Template; use superbig\vipps\Vipps; +use Twig\Markup; /** * Vipps Utility @@ -24,9 +25,6 @@ */ class VippsVariable { - // Public Methods - // ========================================================================= - public function getOrderDetails(Order $order) { return Vipps::$plugin->getPayments()->getOrderDetails($order); @@ -36,7 +34,7 @@ public function getOrderDetails(Order $order) * @param null $purchasable * */ - public function getExpressButton($purchasable = null, array $config = []): \Twig\Markup + public function getExpressButton($purchasable = null, array $config = []): Markup { $html = Vipps::$plugin->getExpress()->getButton($purchasable, $config); @@ -52,7 +50,7 @@ public function getExpressUrl($purchasable = null, array $config = []): string * @param null $purchasable * */ - public function getExpressFormButton($purchasable = null, array $config = []): \Twig\Markup + public function getExpressFormButton($purchasable = null, array $config = []): Markup { $html = Vipps::$plugin->getExpress()->getFormButton($purchasable, $config);