diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..601afbc --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: tuyennn +ko_fi: thinghost76 +custom: ["https://www.paypal.me/thinghost"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9f4f3ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Technical issue with the Module + +--- + +### Preconditions (*) + +1. +2. + +### Steps to reproduce (*) + +1. +2. + +### Expected result (*) + +1. [Screenshots, logs or description] +2. + +### Actual result (*) + +1. [Screenshots, logs or description] +2. diff --git a/.github/Screenshot/kbank_embedded_full_payment.png b/.github/Screenshot/kbank_embedded_full_payment.png new file mode 100644 index 0000000..5131aaf Binary files /dev/null and b/.github/Screenshot/kbank_embedded_full_payment.png differ diff --git a/.github/Screenshot/kbank_embedded_installment.png b/.github/Screenshot/kbank_embedded_installment.png new file mode 100644 index 0000000..67ba7d9 Binary files /dev/null and b/.github/Screenshot/kbank_embedded_installment.png differ diff --git a/.github/Screenshot/kbank_payment_configuration.png b/.github/Screenshot/kbank_payment_configuration.png new file mode 100644 index 0000000..cd76a10 Binary files /dev/null and b/.github/Screenshot/kbank_payment_configuration.png differ diff --git a/.github/workflows/coding-standard.yml b/.github/workflows/coding-standard.yml new file mode 100644 index 0000000..885022d --- /dev/null +++ b/.github/workflows/coding-standard.yml @@ -0,0 +1,14 @@ +name: ExtDN M2 Coding Standard +on: + push: + branches: + - master + pull_request: + +jobs: + static: + name: M2 Coding Standard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: extdn/github-actions-m2/magento-coding-standard@master diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..338fc72 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,119 @@ +name: ExtDN M2 Integration Tests +on: [ push, pull_request ] + +jobs: + mage247: + name: Magento 2 Integration Tests (Magento v2.4.7) + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --tmpfs /tmp:rw --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + es: + image: docker.io/wardenenv/elasticsearch:8.1 + ports: + - 9200:9200 + env: + 'discovery.type': single-node + 'xpack.security.enabled': false + ES_JAVA_OPTS: "-Xms64m -Xmx512m" + options: --health-cmd="curl localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: M2 Integration Tests with Magento 2 + uses: extdn/github-actions-m2/magento-integration-tests/8.3@master + with: + module_name: GhoSter_KbankPayments + composer_name: ghoster/module-kbankpayments + ce_version: 2.4.7 + + mage246: + name: Magento 2 Integration Tests (Magento v2.4.6) + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --tmpfs /tmp:rw --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + es: + image: docker.io/wardenenv/elasticsearch:8.1 + ports: + - 9200:9200 + env: + 'discovery.type': single-node + 'xpack.security.enabled': false + ES_JAVA_OPTS: "-Xms64m -Xmx512m" + options: --health-cmd="curl localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: M2 Integration Tests with Magento 2 + uses: extdn/github-actions-m2/magento-integration-tests/8.2@master + with: + module_name: GhoSter_KbankPayments + composer_name: ghoster/module-kbankpayments + ce_version: 2.4.6 + + mage244: + name: Magento 2 Integration Tests (Magento v2.4.4) + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --tmpfs /tmp:rw --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + es: + image: docker.io/wardenenv/elasticsearch:7.16 + ports: + - 9200:9200 + env: + 'discovery.type': single-node + 'xpack.security.enabled': false + ES_JAVA_OPTS: "-Xms64m -Xmx512m" + options: --health-cmd="curl localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: M2 Integration Tests with Magento 2 + uses: extdn/github-actions-m2/magento-integration-tests/7.4@master + with: + module_name: GhoSter_KbankPayments + composer_name: ghoster/module-kbankpayments + ce_version: 2.4.4 + + mage24: + name: Magento 2 Integration Tests (Magento v2.4.3) + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: --tmpfs /tmp:rw --tmpfs /var/lib/mysql:rw --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + es: + image: docker.io/wardenenv/elasticsearch:7.10 + ports: + - 9200:9200 + env: + 'discovery.type': single-node + 'xpack.security.enabled': false + ES_JAVA_OPTS: "-Xms64m -Xmx512m" + options: --health-cmd="curl localhost:9200/_cluster/health?wait_for_status=yellow&timeout=60s" --health-interval=10s --health-timeout=5s --health-retries=3 + steps: + - uses: actions/checkout@v2 + - name: M2 Integration Tests with Magento 2 + uses: extdn/github-actions-m2/magento-integration-tests/7.4@master + with: + module_name: GhoSter_KbankPayments + composer_name: ghoster/module-kbankpayments + ce_version: 2.4.3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f66850f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.buildpath +.project +.settings/ +.idea/ \ No newline at end of file diff --git a/Api/Data/MetaInterface.php b/Api/Data/MetaInterface.php new file mode 100644 index 0000000..110b081 --- /dev/null +++ b/Api/Data/MetaInterface.php @@ -0,0 +1,490 @@ +_backendConfig = $backendConfig; + parent::__construct($context, $authSession, $jsHelper, $data); + } + + /** + * Add custom css class + * + * @param AbstractElement $element + * @return string + */ + protected function _getFrontendClass($element) + { + return parent::_getFrontendClass($element) . ' with-button enabled'; + } + + /** + * Return header title part of html for payment solution + * + * @param AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function _getHeaderTitleHtml($element) + { + $html = '
'; + + $groupConfig = $element->getGroup(); + + $htmlId = $element->getHtmlId(); + $html .= '
'; + + $html .= '
'; + $html .= '
' . $element->getLegend() . ''; + + if ($element->getComment()) { + $html .= '' . $element->getComment() . ''; + } + $html .= '
'; + $html .= '
'; + + return $html; + } + + /** + * Return header comment part of html for payment solution + * + * @param AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _getHeaderCommentHtml($element) + { + return ''; + } + + /** + * Get collapsed state on-load + * + * @param AbstractElement $element + * @return false + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _isCollapseState($element) + { + return false; + } + + /** + * Extra javascript + * + * @param AbstractElement $element + * @return string + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + protected function _getExtraJs($element) + { + $script = "require(['jquery', 'prototype'], function(jQuery){ + window.toggleSolution = function (id, url) { + var doScroll = false; + Fieldset.toggleCollapse(id, url); + if ($(this).hasClassName(\"open\")) { + $$(\".with-button button.button\").each(function(anotherButton) { + if (anotherButton != this && $(anotherButton).hasClassName(\"open\")) { + $(anotherButton).click(); + doScroll = true; + } + }.bind(this)); + } + if (doScroll) { + var pos = Element.cumulativeOffset($(this)); + window.scrollTo(pos[0], pos[1] - 45); + } + } + });"; + + return $this->_jsHelper->getScript($script); + } +} diff --git a/Block/Adminhtml/Config/Form/Field/Webhook.php b/Block/Adminhtml/Config/Form/Field/Webhook.php new file mode 100644 index 0000000..1f6ed8d --- /dev/null +++ b/Block/Adminhtml/Config/Form/Field/Webhook.php @@ -0,0 +1,66 @@ +storeManager = $storeManager; + $this->request = $request; + parent::__construct($context, $data); + } + + /** + * Get Element Html + * + * @param AbstractElement $element + * @return string + * @throws NoSuchEntityException + */ + protected function _getElementHtml(AbstractElement $element) + { + $storeId = $this->request->getParam('website') ?? $this->request->getParam('store'); + + if ($storeId) { + $this->storeManager->setCurrentStore($storeId); + } + + return $this->storeManager->getStore()->getBaseUrl() . self::URI; + } +} diff --git a/Block/Adminhtml/System/Config/SmartPayId.php b/Block/Adminhtml/System/Config/SmartPayId.php new file mode 100644 index 0000000..05ff091 --- /dev/null +++ b/Block/Adminhtml/System/Config/SmartPayId.php @@ -0,0 +1,91 @@ +addColumn( + 'smartpay_id', + [ + 'label' => __('SMARTPAY ID'), + 'renderer' => false + ] + ); + $this->addColumn( + 'payment_term', + [ + 'label' => __('PAYMENT TERM'), + 'renderer' => false + ] + ); + $this->addColumn( + 'installment_title', + [ + 'label' => __('Installment Title'), + 'renderer' => false + ] + ); + $this->addColumn( + 'status', + [ + 'label' => __('Status'), + 'renderer' => $this->_getActivationRenderer() + ] + ); + $this->_addAfter = false; + $this->_addButtonLabel = __('Add Installment'); + } + + /** + * Get activation options. + * + * @return Activation + * @throws LocalizedException + */ + protected function _getActivationRenderer(): Activation + { + if (!$this->activation) { + $this->activation = $this->getLayout()->createBlock( + Activation::class, + '', + ['data' => ['is_render_to_js_template' => true]] + ); + } + + return $this->activation; + } + + /** + * Prepare existing row data object. + * + * @param DataObject $row + * @return void + * @throws LocalizedException + */ + protected function _prepareArrayRow(DataObject $row) + { + $options = []; + $customAttribute = $row->getData('status'); + + $key = 'option_' . $this->_getActivationRenderer()->calcOptionHash($customAttribute); + $options[$key] = 'selected = "selected"'; + $row->setData('option_extra_attrs', $options); + } +} diff --git a/Block/Adminhtml/System/Config/SmartPayId/Activation.php b/Block/Adminhtml/System/Config/SmartPayId/Activation.php new file mode 100644 index 0000000..289ed9b --- /dev/null +++ b/Block/Adminhtml/System/Config/SmartPayId/Activation.php @@ -0,0 +1,61 @@ +enableDisable = $enableDisable; + } + + /** + * Set Input Name + * + * @param string $value + * @return $this + */ + public function setInputName($value) + { + return $this->setName($value); + } + + /** + * Parse to html. + * + * @return mixed + */ + public function _toHtml() + { + if (!$this->getOptions()) { + $attributes = $this->enableDisable->toOptionArray(); + + foreach ($attributes as $attribute) { + $this->addOption($attribute['value'], $attribute['label']); + } + } + + return parent::_toHtml(); + } +} diff --git a/Block/Form.php b/Block/Form.php new file mode 100644 index 0000000..cbb827b --- /dev/null +++ b/Block/Form.php @@ -0,0 +1,67 @@ +getData('method'); + + if (!$method instanceof MethodInterface) { + throw new LocalizedException( + __('We cannot retrieve the payment method model object.') + ); + } + return $method; + } + + /** + * Sets payment method instance to form + * + * @param MethodInterface $method + * @return Form + */ + public function setMethod(MethodInterface $method) + { + $this->setData('method', $method); + return $this; + } + + /** + * Retrieve payment method code + * + * @return string + * @throws LocalizedException + */ + public function getMethodCode() + { + return $this->getMethod()->getCode(); + } + + /** + * Retrieve field value data from payment info object + * + * @param mixed $field + * @return string + * @throws LocalizedException + */ + public function getInfoData($field) + { + return $this->escapeHtml($this->getMethod()->getInfoInstance()->getData($field)); + } +} diff --git a/Block/Info.php b/Block/Info.php new file mode 100644 index 0000000..af11cc7 --- /dev/null +++ b/Block/Info.php @@ -0,0 +1,23 @@ +resultPageFactory = $resultPageFactory; + $this->resultForwardFactory = $resultForwardFactory; + $this->checkoutSession = $checkoutSession; + $this->orderFactory = $orderFactory; + $this->checkoutHelper = $checkoutHelper; + $this->metaRepository = $metaRepository; + $this->logger = $logger; + $this->dataObjectFactory = $dataObjectFactory; + $this->kbankConfig = $kbankConfig ?: ObjectManager::getInstance() + ->get(KbankConfig::class); + $this->paymentFailures = $paymentFailures ?: ObjectManager::getInstance() + ->get(PaymentFailuresInterface::class); + + $this->metaDataFactory = $metaDataFactory ?: ObjectManager::getInstance() + ->get(MetaInterfaceFactory::class); + + $this->url = $url ?: ObjectManager::getInstance() + ->get(Url::class); + $this->httpHeader = $header ?: ObjectManager::getInstance() + ->get(Header::class); + parent::__construct($context); + } +} diff --git a/Controller/Payment/Callback.php b/Controller/Payment/Callback.php new file mode 100644 index 0000000..39a2872 --- /dev/null +++ b/Controller/Payment/Callback.php @@ -0,0 +1,204 @@ +resultRedirectFactory->create(); + + $this->_eventManager->dispatch( + 'none_samesite_response_before', + ['request' => $this->getRequest(), 'header' => $this->httpHeader] + ); + + $validResponse = $this->_verifyResponse(); + + if ($this->chargeId) { + try { + $this->meta = $this->metaRepository->getByChargeId($this->chargeId); + + $this->order = $this->orderFactory->create() + ->loadByIncrementId($this->meta->getOrderId()); + + $this->url->setOrder($this->order); + } catch (\Exception $e) { + $this->errors[] = __('Meta was mismatched or empty.'); + } + + if (!$this->order) { + $this->errors[] = __('Order was mismatched or empty.'); + } + + if ($validResponse && $this->order instanceof Order && $this->meta) { + try { + if (!$this->_checkOrderState($this->order)) { + $this->logger + ->info( + sprintf( + 'Order status already changed. Do nothing: %s', + $this->order->getStatus() + ) + ); + } else { + $errorObject = $this->dataObjectFactory + ->create( + [ + 'data' => ['error' => $this->errors] + ] + ); + + $this->_eventManager->dispatch( + 'kbank_embedded_payment_method_callback_validation', + [ + 'meta' => $this->meta, + 'order' => $this->order, + 'error' => $errorObject, + 'type' => 'callback' + ] + ); + + $this->errors = $errorObject->getData('error'); + + if (!in_array($this->order->getStatus(), $this->processedOrderStates)) { + $this->errors[] = __('Callback Order was not processed correctly or order was cancelled'); + } + } + } catch (\Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + } + + if (!empty($this->errors)) { + $comment = implode(',', $this->errors); + $this->messageManager->addErrorMessage($comment); + if ($this->order) { + $this->order->cancel(); + $this->order->save(); + } + } + + $resultRedirect->setUrl(empty($this->errors) ? $this->url->getSuccessUrl() : $this->url->getFailureUrl()); + return $resultRedirect; + } + + /** + * Verify the response + * + * @return bool + */ + protected function _verifyResponse(): bool + { + $responseData = $this->getRequest()->getPostValue(); + + if ($this->kbankConfig->isDebugMode()) { + $this->logger->info('Header Callback response data from Callback Controller'); + $this->logger->info($this->getRequest()->getHeader('Content-Type')); + $this->logger->info('Callback response data from Callback Controller'); + $this->logger->info($responseData); + } + + $this->chargeId = $responseData['objectId'] ?? false; + $this->token = $responseData['token'] ?? false; + $status = $responseData['status'] ?? Config::CALLBACK_INVALID_STATUS; + + if ($status !== Config::CALLBACK_VALID_STATUS) { + $this->logger->info(sprintf('Callback Controller: Status was mismatched or empty - %s', $status)); + $this->errors[] = __('Status was mismatched or empty.'); + return false; + } + + if (!$this->chargeId) { + $this->logger + ->info( + sprintf('Callback Controller: Charge Id was mismatched or empty. - %s', $this->chargeId) + ); + $this->errors[] = __('Charge Id was mismatched or empty.'); + return false; + } + + if (!$this->token) { + $this->logger->info(sprintf('Callback Controller: Token was mismatched or empty. - %s', $this->token)); + $this->errors[] = __('Token was mismatched or empty.'); + return false; + } + + return true; + } + + /** + * Check order state + * + * @param Order $order + * @return bool + */ + protected function _checkOrderState($order): bool + { + if (!$order) { + return false; + } + + return in_array($order->getState(), $this->allowedOrderStates); + } + + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * Disable Magento's CSRF validation. + * + * @param RequestInterface $request + * @return bool|null + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } +} diff --git a/Controller/Token/Process.php b/Controller/Token/Process.php new file mode 100644 index 0000000..6029226 --- /dev/null +++ b/Controller/Token/Process.php @@ -0,0 +1,129 @@ +resultRedirectFactory->create(); + + $token = $this->getRequest()->getPostValue('token'); + $lastRealOrderId = $this->getRequest()->getParam('order_id'); + + if (!$lastRealOrderId) { + $lastRealOrderId = $this->checkoutSession->getLastRealOrderId(); + } + + if (!!$lastRealOrderId) { + /** @var Order $order */ + $this->order = $this->orderFactory->create()->loadByIncrementId($lastRealOrderId); + } + + $orderIncrementId = $this->order instanceof Order ? $this->order->getIncrementId() : 0; + + if (!empty($orderIncrementId) + && !empty($token) + ) { + $this->url->setOrder($this->order); + + $storeId = $this->order->getStoreId(); + + $result = $this->dataObjectFactory->create() + ->addData(['result' => true, 'message' => '']); + + $meta = $this->metaDataFactory->create(); + + $this->_eventManager->dispatch( + 'kbank_embedded_payment_method_process_token_data', + [ + 'gateway_token' => $this->getRequest()->getPostValue(), + 'order' => $this->order, + 'result' => $result, + 'meta' => $meta + ] + ); + + if ($this->_checkOrderState($this->order) + && !$this->kbankConfig->is3DS($storeId) + && $result->getData('result') === true + ) { + /** Non 3DS */ + $resultRedirect->setUrl($this->url->getSuccessUrl()); + } elseif ($this->_checkOrderState($this->order) + && $this->kbankConfig->is3DS($storeId) + && $result->getData('result') === true + ) { + /** 3DS Support */ + try { + $meta = $this->metaRepository->getByOrderIncrement($this->order->getIncrementId()); + if ($redirectUrl = $meta->getRedirectUrl()) { + $resultRedirect->setUrl($redirectUrl); + return $resultRedirect; + } + } catch (\Exception $e) { + $this->messageManager->addErrorMessage(__('Your payment has been declined. Please try again.')); + $resultRedirect->setUrl($this->url->getFailureUrl()); + } + } else { + $this->messageManager->addErrorMessage(__('Something went wrong while processing your order.')); + $resultRedirect->setUrl($this->url->getFailureUrl()); + } + } + + return $resultRedirect; + } + + /** + * Check order state + * + * @param Order $order + * @return bool + */ + protected function _checkOrderState(Order $order): bool + { + return in_array($order->getState(), $this->allowedOrderStates); + } + + /** + * @inheritDoc + */ + public function createCsrfValidationException( + RequestInterface $request + ): ?InvalidRequestException { + return null; + } + + /** + * Disable Magento's CSRF validation. + * + * @param RequestInterface $request + * @return bool|null + */ + public function validateForCsrf(RequestInterface $request): ?bool + { + return true; + } +} diff --git a/Cron/CancelExpiredPendingOrder.php b/Cron/CancelExpiredPendingOrder.php new file mode 100644 index 0000000..89be78e --- /dev/null +++ b/Cron/CancelExpiredPendingOrder.php @@ -0,0 +1,222 @@ +transactionProcessor = $transactionProcessor; + $this->orderManagement = $orderManagement; + $this->orderCollectionFactory = $orderCollectionFactory; + $this->orderFactory = $orderFactory; + $this->metaRepository = $metaRepository; + $this->iterator = $iterator; + $this->kbankEmbeddedConfig = $kbankEmbeddedConfig; + $this->orderRepository = $orderRepository; + $this->logger = $logger; + } + + /** + * Cancel the deprecated order + * + * @return $this + */ + public function execute() + { + $expiredPendingMinutes = $this->kbankEmbeddedConfig->getExpiredPendingTimes(); + + if (!$expiredPendingMinutes) { + return $this; + } + + /** @var $orders OrderCollection */ + $orders = $this->orderCollectionFactory->create(); + $orders->addFieldToFilter('status', ['in' => ['pending', 'pending_payment']]); + $orders->getSelect()->where( + new \Zend_Db_Expr( + 'TIME_TO_SEC(TIMEDIFF(CURRENT_TIMESTAMP, `updated_at`)) >= ' . $expiredPendingMinutes * 60 + ) + ); + + $orders->getSelect() + ->join( + ["sop" => "sales_order_payment"], + 'main_table.entity_id = sop.parent_id', + ['method'] + ) + ->where( + 'sop.method IN (?)', + [ + KbankEmbeddedConfig::METHOD_KBANK_INLINE, + KbankEmbeddedConfig::METHOD_KBANK_EMBEDDED_INSTALLMENT, + KbankEmbeddedConfig::METHOD_KBANK_EMBEDDED_FULLPAYMENT + ] + ) + ->limit(5); + + $orders->setOrder( + 'created_at', + 'desc' + ); + + if ($orders->getSize() > 0) { + $this->iterator->walk( + $orders->getSelect(), + [[$this, 'callbackUpdateKbankOrders']], + [ + 'order' => $this->orderFactory->create() + ] + ); + } + + return $this; + } + + /** + * Callback update + * + * @param array $args + * @throws \Exception + */ + public function callbackUpdateKbankOrders($args) + { + /** @var Order $order */ + $order = $args['order']; + $order->setData($args['row']); + + try { + $meta = $this->metaRepository->getByOrderIncrement($order->getIncrementId()); + $resultResponse = $this->transactionProcessor + ->transactionDetail($order, $meta->getChargeId()); + + $this->processInquiryResponse($order, $resultResponse); + } catch (\Exception $e) { + $this->logger->error('Error while processing deprecated orders: ' . $e->getMessage()); + $order->cancel(); + $this->orderRepository->save($order); + } finally { + return; + } + } + + /** + * Try to Inquiry order before cancel + * + * @param Order $order + * @param array $resultResponse + */ + protected function processInquiryResponse(Order $order, array $resultResponse) + { + if (!isset($resultResponse['object']) + || !isset($resultResponse['transaction_state']) + ) { + $order->addCommentToStatusHistory( + __("Invalid returned while inquiring or invalid transaction state"), + $order->getStatus(), + )->setIsCustomerNotified(false); + + $order->cancel(); + } + + if ($resultResponse['object'] == KbankEmbeddedConfig::VALIDATION_3DS_VALIDATION_OBJECT + && $resultResponse['transaction_state'] == KbankEmbeddedConfig::TRANSACTION_STATE_AUTH + ) { + $order->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $order->addStatusToHistory($order->getStatus(), __('Payment re-Authorized automatically.')); + + if ($this->kbankEmbeddedConfig->isAutoInvoice() + && $order->canInvoice() + ) { + $this->transactionProcessor->createInvoice($order); + } + } else { + $order->addCommentToStatusHistory( + __("Invalid returned while inquiring or invalid transaction state"), + $order->getStatus(), + )->setIsCustomerNotified(false); + $order->cancel(); + } + + $this->orderRepository->save($order); + } +} diff --git a/Gateway/Command/AuthorizeCommand.php b/Gateway/Command/AuthorizeCommand.php new file mode 100644 index 0000000..8bbf248 --- /dev/null +++ b/Gateway/Command/AuthorizeCommand.php @@ -0,0 +1,91 @@ +commandPool = $commandPool; + $this->transactionRepository = $repository; + $this->filterBuilder = $filterBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject) + { + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($commandSubject); + + $command = $this->getCommand($paymentDO); + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Get execution command name. + * + * @param PaymentDataObjectInterface $paymentDO + * @return string + */ + private function getCommand(PaymentDataObjectInterface $paymentDO) + { + $payment = $paymentDO->getPayment(); + ContextHelper::assertOrderPayment($payment); + return self::AUTHORIZE; + } +} diff --git a/Gateway/Command/CancelCommand.php b/Gateway/Command/CancelCommand.php new file mode 100644 index 0000000..2e60cf7 --- /dev/null +++ b/Gateway/Command/CancelCommand.php @@ -0,0 +1,92 @@ +request = $request; + $this->validator = $validator; + $this->logger = $logger; + $this->handler = $handler; + } + + /** + * Executes command basing on business object + * + * @param array $commandSubject + * @return null|Command\ResultInterface + * @throws CommandException + */ + public function execute(array $commandSubject) + { + $response = $this->request->getParams(); + /** @var Payment $payment */ + $result = $this->validator->validate( + array_merge($commandSubject, ['response' => $response]) + ); + + if (!$result->isValid()) { + $this->logExceptions($result->getFailsDescription()); + throw new CommandException( + __('Transaction has been declined. Please try again later.') + ); + } + + $this->handler->handle( + array_merge($commandSubject), + $response + ); + } + + /** + * Log exceptions + * + * @param Phrase[] $fails + * @return void + */ + private function logExceptions(array $fails) + { + foreach ($fails as $failPhrase) { + $this->logger->info((string) $failPhrase); + } + } +} diff --git a/Gateway/Command/CaptureCommand.php b/Gateway/Command/CaptureCommand.php new file mode 100644 index 0000000..4926e35 --- /dev/null +++ b/Gateway/Command/CaptureCommand.php @@ -0,0 +1,139 @@ +commandPool = $commandPool; + $this->transactionRepository = $repository; + $this->filterBuilder = $filterBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject) + { + /** @var PaymentDataObjectInterface $paymentDO */ + $paymentDO = $this->subjectReader->readPayment($commandSubject); + + $command = $this->getCommand($paymentDO); + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Get execution command name. + * + * @TODO: Need to implement CAPTURE as Settle; + * Settling an order only can be done after 1 day order authorized; + * + * @param PaymentDataObjectInterface $paymentDO + * @return string + */ + private function getCommand(PaymentDataObjectInterface $paymentDO) + { + $payment = $paymentDO->getPayment(); + ContextHelper::assertOrderPayment($payment); + + // If auth transaction does not exist then execute authorize&capture command + $captureExists = $this->captureTransactionExists($payment); + if (!$payment->getAuthorizationTransaction() && !$captureExists) { + return self::AUTHORIZE; + } + + return self::DO_NOTHING; + //return self::CAPTURE; + } + + /** + * Check if capture transaction already exists + * + * @param OrderPaymentInterface $payment + * @return bool + */ + private function captureTransactionExists(OrderPaymentInterface $payment) + { + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('payment_id') + ->setValue($payment->getId()) + ->create(), + ] + ); + + $this->searchCriteriaBuilder->addFilters( + [ + $this->filterBuilder + ->setField('txn_type') + ->setValue(TransactionInterface::TYPE_CAPTURE) + ->create(), + ] + ); + + $searchCriteria = $this->searchCriteriaBuilder->create(); + $count = $this->transactionRepository->getList($searchCriteria) + ->getTotalCount(); + + return $count > 0; + } +} diff --git a/Gateway/Command/DoNothingCommand.php b/Gateway/Command/DoNothingCommand.php new file mode 100644 index 0000000..b8a71c2 --- /dev/null +++ b/Gateway/Command/DoNothingCommand.php @@ -0,0 +1,16 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + $this->config = $config; + $this->handler = $handler; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject) + { + $command = $this->commandPool->get('get_transaction_details'); + $result = $command->execute($commandSubject); + $response = $result->get(); + + if ($this->handler) { + $this->handler->handle($commandSubject, $response); + } + + $rawDetails = $source = []; + foreach ($response as $key => &$value) { + if (!$value || empty($value)) { + $value = ''; + } + + if ($key == 'source') { + $source = $value; + } + + if (!is_array($value)) { + $rawDetails[$key] = $response[$key]; + } + } + + return array_merge($source, $rawDetails); + } +} diff --git a/Gateway/Command/GatewayQueryCommand.php b/Gateway/Command/GatewayQueryCommand.php new file mode 100644 index 0000000..e97c9f9 --- /dev/null +++ b/Gateway/Command/GatewayQueryCommand.php @@ -0,0 +1,94 @@ +requestBuilder = $requestBuilder; + $this->transferFactory = $transferFactory; + $this->client = $client; + $this->validator = $validator; + $this->logger = $logger; + } + + /** + * @inheritdoc + * + * @throws Exception + */ + public function execute(array $commandSubject) + { + $transferO = $this->transferFactory->create( + $this->requestBuilder->build($commandSubject) + ); + + try { + $response = $this->client->placeRequest($transferO); + } catch (Exception $e) { + $this->logger->critical($e); + + throw new CommandException(__('There was an error while trying to process the request.')); + } + + $result = $this->validator->validate( + array_merge($commandSubject, ['response' => $response]) + ); + if (!$result->isValid()) { + throw new CommandException(__('There was an error while trying to process the request.')); + } + + return new ArrayResult($response); + } +} diff --git a/Gateway/Command/InitializeCommand.php b/Gateway/Command/InitializeCommand.php new file mode 100644 index 0000000..6788d39 --- /dev/null +++ b/Gateway/Command/InitializeCommand.php @@ -0,0 +1,41 @@ +getPayment(); + $order = $payment->getOrder(); + ContextHelper::assertOrderPayment($payment); + if (!$payment instanceof Payment) { + throw new \LogicException('Order Payment should be provided'); + } + + $payment->setAmountAuthorized($order->getTotalDue()); + $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); + + $payment->getOrder()->setCanSendNewEmailFlag(false); + $stateObject->setData(OrderInterface::STATE, Order::STATE_PENDING_PAYMENT); + $stateObject->setData(OrderInterface::STATUS, Order::STATE_PENDING_PAYMENT); + $stateObject->setData('is_notified', false); + } +} diff --git a/Gateway/Command/RefundTransactionStrategyCommand.php b/Gateway/Command/RefundTransactionStrategyCommand.php new file mode 100644 index 0000000..46d8ba1 --- /dev/null +++ b/Gateway/Command/RefundTransactionStrategyCommand.php @@ -0,0 +1,72 @@ +commandPool = $commandPool; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function execute(array $commandSubject) + { + $command = $this->getCommand($commandSubject); + + $this->commandPool->get($command) + ->execute($commandSubject); + } + + /** + * Determines the command that should be used based on the status of the transaction + * + * @param array $commandSubject + * @return string + * @throws CommandException|NotFoundException + */ + private function getCommand(array $commandSubject) + { + $details = $this->commandPool->get('get_transaction_details') + ->execute($commandSubject) + ->get(); + + if ($details['transaction_state'] === \GhoSter\KbankPayments\Gateway\Config::TRANSACTION_STATE_AUTH) { + return self::VOID; + } elseif ($details['transaction_state'] !== \GhoSter\KbankPayments\Gateway\Config::TRANSACTION_STATE_SETTLED) { + throw new CommandException(__('This transaction cannot be refunded with its current status.')); + } + + return self::REFUND; + } +} diff --git a/Gateway/Config.php b/Gateway/Config.php new file mode 100644 index 0000000..5f985de --- /dev/null +++ b/Gateway/Config.php @@ -0,0 +1,315 @@ +getValue(Config::KEY_ACTIVATE, $storeId); + } + + /** + * Gets the login id + * + * @param int|null $storeId + * @return string + */ + public function getPublicKey(?int $storeId = null): ?string + { + $publicKey = $this->getGeneralConfig(Config::KEY_PUBLIC_KEY, $storeId); + + if (mb_detect_encoding($publicKey) !== 'ASCII') { + return ''; + } + + return $publicKey; + } + + /** + * Gets the secret key id + * + * @param ?int $storeId + * @return string + */ + public function getSecretKey(?int $storeId = null): ?string + { + $secretKey = $this->getGeneralConfig(Config::KEY_SECRET_KEY, $storeId); + + if (mb_detect_encoding($secretKey) !== 'ASCII') { + return ''; + } + + return $secretKey; + } + + /** + * Get Shop name + * + * @param ?int $storeId + * @return string + */ + public function getShopName(?int $storeId = null): ?string + { + return $this->getGeneralConfig(Config::KEY_SHOP_NAME, $storeId); + } + + /** + * Gets the current environment + * + * @param ?int $storeId + * @return string + */ + public function getEnvironment(?int $storeId = null): ?string + { + $config = $this->getGeneralConfig(Config::KEY_ENVIRONMENT, $storeId); + return $config ?? Environment::ENVIRONMENT_SANDBOX; + } + + /** + * Is Debug mode + * + * @param ?int $storeId + * @return bool + */ + public function isDebugMode(?int $storeId = null): bool + { + return (bool)$this->getGeneralConfig(Config::KEY_DEBUG, $storeId); + } + + /** + * Gets the API endpoint URL + * + * @param ?int $storeId + * @return string + */ + public function getApiUrl(?int $storeId = null): ?string + { + $environment = $this->getEnvironment($storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::ENDPOINT_URL_SANDBOX + : self::ENDPOINT_URL_PRODUCTION; + } + + /** + * Gets the API js embedded endpoint URL + * + * @param ?int $storeId + * @return string + */ + public function getJsSrc(?int $storeId = null): ?string + { + $environment = $this->getEnvironment($storeId); + + return $environment === Environment::ENVIRONMENT_SANDBOX + ? self::ENDPOINT_JS_URL_SANDBOX + : self::ENDPOINT_JS_URL_PRODUCTION; + } + + /** + * Expired time + * + * @param ?int $storeId + * @return int + */ + public function getExpiredPendingTimes(?int $storeId = null): ?int + { + return $this->getGeneralConfig(Config::KEY_EXPIRED_PENDING_TIME, $storeId); + } + + /** + * Pre-defined Installment Selection + * + * @param ?int $storeId + * @return bool + */ + public function isPredefinedInstallmentSelection(?int $storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_PRE_DEFINED_INSTALLMENT_SELECTION, $storeId); + } + + /** + * Merchant ID + * + * @param ?int $storeId + * @return string|null + */ + public function getMerchantId(?int $storeId = null): ?string + { + return (string)$this->getValue(Config::KEY_MERCHANT_ID, $storeId); + } + + /** + * Is 3ds + * + * @param ?int $storeId + * @return bool + */ + public function is3DS(?int $storeId = null): bool + { + return (bool)$this->getGeneralConfig(Config::KEY_3DS_SUPPORT, $storeId); + } + + /** + * Is auto invoice + * + * @param ?int $storeId + * @return bool + */ + public function isAutoInvoice(?int $storeId = null): bool + { + return (bool)$this->getGeneralConfig(Config::KEY_AUTO_INVOICE, $storeId); + } + + /** + * Get custom frontend response + * + * @param int|null $storeId + * @return string + */ + public function getFrontendResponseUrl(?int $storeId = null): string + { + if ($this->is3DS($storeId)) { + return ''; + } + return (string)$this->getGeneralConfig(Config::KEY_FRONTEND_RESPONSE, $storeId); + } + + /** + * Installment Support enabling + * + * @param ?int $storeId + * @return bool + */ + public function isInstallmentSupport(?int $storeId = null): bool + { + return (bool)$this->getValue(Config::KEY_INSTALLMENT_SUPPORT, $storeId); + } + + /** + * Get Terminal Info + * + * @param int|null $storeId + * @return string|null + */ + public function getTerminalId(?int $storeId = null): ?string + { + return (string)$this->getValue(Config::KEY_TERMINAL_ID, $storeId); + } + + /** + * Installment Information + * + * @param ?int $storeId + * @return string|null + */ + public function getInstallmentInfo(?int $storeId = null): ?string + { + return $this->getValue(Config::KEY_INSTALLMENT_INFO, $storeId); + } + + /** + * Get scope config + * + * @return ScopeConfigInterface + * @deprecated 100.0.10 + * @see \Magento\Framework\App\ObjectManager\ConfigWriterInterface + */ + protected function getScopeConfig(): ScopeConfigInterface + { + if ($this->scopeConfig === null) { + $this->scopeConfig = ObjectManager::getInstance()->get( + ScopeConfigInterface::class + ); + } + + return $this->scopeConfig; + } + + /** + * General configuration + * + * @param mixed $field + * @param ?int $storeId + * @return mixed + */ + public function getGeneralConfig($field, ?int $storeId = null) + { + return $this->getScopeConfig()->getValue( + sprintf(self::GENERAL_PATH_PATTERN, $field), + ScopeInterface::SCOPE_STORE, + $storeId + ); + } + + /** + * Specific payment configuration value + * + * @param string $paymentCode + * @param string $field + * @param int|null $storeId + * @return mixed + */ + public function getSpecificPaymentConfig(string $paymentCode, string $field, ?int $storeId = null) + { + return $this->getScopeConfig()->getValue( + sprintf(self::PAYMENT_PATH_PATTERN, $paymentCode, $field), + ScopeInterface::SCOPE_STORE, + $storeId + ); + } +} diff --git a/Gateway/Http/Client.php b/Gateway/Http/Client.php new file mode 100644 index 0000000..f211e4f --- /dev/null +++ b/Gateway/Http/Client.php @@ -0,0 +1,201 @@ +httpClientFactory = $httpClientFactory; + $this->config = $config; + $this->logger = $logger; + $this->json = $json; + } + + /** + * Places request to gateway. Returns result as ENV array + * + * @param TransferInterface $transferObject + * @return array + * @throws ClientException + */ + public function placeRequest(TransferInterface $transferObject) + { + try { + $request = $transferObject->getBody(); + $this->logData = [ + 'request' => $request, + ]; + + return $this->postRequest($request); + // phpcs:ignore Magento2.Exceptions.ThrowCatch + } catch (\Exception $e) { + $this->logger->critical($e); + + throw new ClientException( + __('Something went wrong in the payment gateway.') + ); + } finally { + $this->logger->debug($this->logData); + } + } + + /** + * Post request + * + * @param array $request + * @return array + * @throws \Zend_Http_Client_Exception + */ + public function postRequest(&$request) + { + $this->logData['request'] = $request; + $payLoadType = $request['payload_type']; + $requestType = $request['method_type']; + + if (isset($request['payload_type'])) { + unset($request['payload_type']); + } + + if (isset($request['method_type'])) { + unset($request['method_type']); + } + + if (!in_array($requestType, ['GET', 'POST', 'PUT', 'DELETE'])) { + //phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Send first parameter must be "GET", "POST", "PUT" or "DELETE"'); + } + + $url = $this->getEndpointUrl($payLoadType, $request); + + try { + + /** @var ZendClient $client */ + $client = $this->httpClientFactory->create(); + + $client->setUri($url); + $client->setConfig(['maxredirects' => 0, 'timeout' => 30]); + $client->setHeaders([ + 'Content-Type: application/json', + 'x-api-key: ' . $this->config->getSecretKey(), + ]); + + $client->setMethod($requestType); + + if (!empty($request)) { + $client->setRawData($this->json->serialize($request), 'application/json'); + } + + $responseBody = $client->request() + ->getBody(); + + $this->logData['response'] = $responseBody; + + $data = $this->json->unserialize($responseBody); + } catch (InvalidArgumentException $e) { + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new \Exception('Invalid JSON was returned by the gateway'); + } finally { + $this->logger->debug($this->logData); + } + + return $data; + } + + /** + * Get Endpoint Url + * + * @param string $payloadType + * @param array $request + * @return string + */ + private function getEndpointUrl($payloadType, &$request) + { + $url = $this->config->getApiUrl(); + + switch ($payloadType) { + case 'createAuthorizeRequest': + $url .= '/card/v2/charge'; + break; + + case 'createSettleRequest': + $url .= '/card/v2/charge/{charge_id}/settle'; + break; + + case 'transactionDetailsRequest': + $url .= '/card/v2/charge/{charge_id}'; + break; + + case 'voidRequest': + $url .= '/card/v2/charge/{charge_id}/void'; + break; + + case 'refundRequest': + $url .= '/card/v2/charge/{charge_id}/refund'; + break; + default: + break; + } + + if (preg_match_all('/{+(.*?)}/', $url, $matches)) { + if (!empty($matches[1])) { + foreach ($matches[1] as $match) { + if (array_key_exists($match, $request)) { + $url = str_replace('{' . $match . '}', $request[$match], $url); + unset($request[$match]); + } + } + } + } + + return $url; + } +} diff --git a/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php b/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php new file mode 100644 index 0000000..60084be --- /dev/null +++ b/Gateway/Http/Payload/Filter/RemoveFieldsFilter.php @@ -0,0 +1,35 @@ +fields = $fields; + } + + /** + * @inheritdoc + */ + public function filter(array $data) + { + foreach ($this->fields as $field) { + unset($data[$field]); + } + + return $data; + } +} diff --git a/Gateway/Http/Payload/FilterInterface.php b/Gateway/Http/Payload/FilterInterface.php new file mode 100644 index 0000000..1809a35 --- /dev/null +++ b/Gateway/Http/Payload/FilterInterface.php @@ -0,0 +1,17 @@ +transferBuilder = $transferBuilder; + $this->payloadFilters = $payloadFilters; + } + + /** + * Builds gateway transfer object + * + * @param array $request + * @return TransferInterface + */ + public function create(array $request) + { + foreach ($this->payloadFilters as $filter) { + $request = $filter->filter($request); + } + + return $this->transferBuilder + ->setBody($request) + ->build(); + } +} diff --git a/Gateway/Request/AuthorizeDataBuilder.php b/Gateway/Request/AuthorizeDataBuilder.php new file mode 100644 index 0000000..de4dc2a --- /dev/null +++ b/Gateway/Request/AuthorizeDataBuilder.php @@ -0,0 +1,54 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + $data = []; + + $tokenValue = $payment->getAdditionalInformation('tokenValue'); + + if ($payment instanceof Payment) { + + $data = [ + 'amount' => $order->getGrandTotalAmount(), + 'currency' => $order->getCurrencyCode(), + 'description' => $order->getOrderIncrementId(), + 'source_type' => 'card', + 'mode' => 'token', + 'reference_order' => $order->getOrderIncrementId(), + 'token' => $tokenValue + ]; + } + + return $data; + } +} diff --git a/Gateway/Request/MethodTypeBuilder.php b/Gateway/Request/MethodTypeBuilder.php new file mode 100644 index 0000000..ebad74d --- /dev/null +++ b/Gateway/Request/MethodTypeBuilder.php @@ -0,0 +1,38 @@ +type = $type; + } + + /** + * Adds the type of the request to the build subject + * + * @param array $buildSubject + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(array $buildSubject) + { + return [ + 'method_type' => $this->type + ]; + } +} diff --git a/Gateway/Request/RefundDataBuilder.php b/Gateway/Request/RefundDataBuilder.php new file mode 100644 index 0000000..bfd79b7 --- /dev/null +++ b/Gateway/Request/RefundDataBuilder.php @@ -0,0 +1,52 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + + $data = []; + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $chargeId = $authTransaction->getAdditionalInformation('charge_id'); + + $data = [ + 'charge_id' => $chargeId, + 'amount' => $order->getGrandTotalAmount(), + 'reason' => 'Refund transaction for reference_order: ' . $order->getOrderIncrementId() + ]; + } + + return $data; + } +} diff --git a/Gateway/Request/RequestTypeBuilder.php b/Gateway/Request/RequestTypeBuilder.php new file mode 100644 index 0000000..ca7b1af --- /dev/null +++ b/Gateway/Request/RequestTypeBuilder.php @@ -0,0 +1,38 @@ +type = $type; + } + + /** + * Adds the type of the request to the build subject + * + * @param array $buildSubject + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(array $buildSubject) + { + return [ + 'payload_type' => $this->type + ]; + } +} diff --git a/Gateway/Request/SettleDataBuilder.php b/Gateway/Request/SettleDataBuilder.php new file mode 100644 index 0000000..960cb63 --- /dev/null +++ b/Gateway/Request/SettleDataBuilder.php @@ -0,0 +1,50 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + $data = []; + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $chargeId = $authTransaction->getAdditionalInformation('charge_id'); + + $data = [ + 'charge_id' => $chargeId, + 'amount' => $order->getGrandTotalAmount(), + 'type' => 'full' + ]; + } + + return $data; + } +} diff --git a/Gateway/Request/SourceTypeBuilder.php b/Gateway/Request/SourceTypeBuilder.php new file mode 100644 index 0000000..3902ec4 --- /dev/null +++ b/Gateway/Request/SourceTypeBuilder.php @@ -0,0 +1,35 @@ +type = $type; + } + + /** + * Adds the type of the request to the build subject + * + * @param array $buildSubject + * @return array + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function build(array $buildSubject) + { + return [ + 'source_type' => $this->type + ]; + } +} diff --git a/Gateway/Request/TransactionDetailsDataBuilder.php b/Gateway/Request/TransactionDetailsDataBuilder.php new file mode 100644 index 0000000..9d8a632 --- /dev/null +++ b/Gateway/Request/TransactionDetailsDataBuilder.php @@ -0,0 +1,68 @@ +subjectReader = $subjectReader; + $this->metaRepository = $metaRepository; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $chargeId = null; + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $chargeId = $authTransaction->getAdditionalInformation('charge_id'); + } + + if (!$chargeId) { + try { + $order = $payment->getOrder(); + $meta = $this->metaRepository->getByOrderIncrement($order->getIncrementId()); + $chargeId = $meta->getChargeId(); + } catch (\Exception $e) { + return [ + 'charge_id' => $chargeId + ]; + } + + } + + return [ + 'charge_id' => $chargeId + ]; + } +} diff --git a/Gateway/Request/VoidDataBuilder.php b/Gateway/Request/VoidDataBuilder.php new file mode 100644 index 0000000..c3d49a6 --- /dev/null +++ b/Gateway/Request/VoidDataBuilder.php @@ -0,0 +1,51 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function build(array $buildSubject) + { + $paymentDO = $this->subjectReader->readPayment($buildSubject); + $payment = $paymentDO->getPayment(); + $order = $paymentDO->getOrder(); + + $data = []; + + if ($payment instanceof Payment) { + $authTransaction = $payment->getAuthorizationTransaction(); + $chargeId = $authTransaction->getAdditionalInformation('charge_id'); + + $data = [ + 'charge_id' => $chargeId, + 'reason' => 'Cancel transaction for reference_order: ' . $order->getOrderIncrementId() + ]; + } + + return $data; + } +} diff --git a/Gateway/Response/Handler/Authorize/AuthTransaction.php b/Gateway/Response/Handler/Authorize/AuthTransaction.php new file mode 100644 index 0000000..268d2ab --- /dev/null +++ b/Gateway/Response/Handler/Authorize/AuthTransaction.php @@ -0,0 +1,96 @@ +subjectReader = $subjectReader; + $this->paymentResource = $paymentResource; + $this->transactionBuilder = $transactionBuilder; + $this->logger = $logger; + } + + /** + * Handle the Auth + * + * @param array $handlingSubject + * @param array $response + */ + public function handle(array $handlingSubject, array $response) + { + try { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $chargeId = $response['id']; + $source = $response['source'] ?? []; + + $order = $payment->getOrder(); + $formattedPrice = $order->getBaseCurrency()->formatTxt( + $order->getGrandTotal() + ); + + $transaction = $this->transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId($chargeId) + ->setAdditionalInformation( + $payment->getTransactionAdditionalInfo() + ) + ->setFailSafe(true) + ->build(TransactionInterface::TYPE_AUTH); + + $payment->setCcLast4($source['card_masking'] ? substr($source['card_masking'], -4) : ''); + $payment->setCcAvsStatus('N/A'); + $payment->setIsTransactionClosed(false); + $payment->addTransactionCommentsToOrder( + $transaction, + __('The Payment Authorization success. The authorized amount is %1.', $formattedPrice) + ); + $payment->setParentTransactionId(null); + } + } catch (\Exception $e) { + $this->logger->info($e->getMessage()); + } + } +} diff --git a/Gateway/Response/Handler/Authorize/OrderStatus.php b/Gateway/Response/Handler/Authorize/OrderStatus.php new file mode 100644 index 0000000..6e3799b --- /dev/null +++ b/Gateway/Response/Handler/Authorize/OrderStatus.php @@ -0,0 +1,62 @@ +subjectReader = $subjectReader; + $this->orderRepository = $orderRepository; + $this->paymentStatus = $paymentStatus; + } + + /** + * Handle + * + * @param array $handlingSubject + * @param array $response + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $order = $payment->getOrder(); + + $order->setState($this->paymentStatus->getOrderStateByPaymentStatus(PaymentStatus::PAYMENT_AUTHORIZED)) + ->setStatus(PaymentStatus::PAYMENT_AUTHORIZED); + $order->addStatusToHistory($order->getStatus(), 'Payment Authorized.'); + $this->orderRepository->save($order); + } + } +} diff --git a/Gateway/Response/Handler/Cancel/CancelOrder.php b/Gateway/Response/Handler/Cancel/CancelOrder.php new file mode 100644 index 0000000..0658b0f --- /dev/null +++ b/Gateway/Response/Handler/Cancel/CancelOrder.php @@ -0,0 +1,52 @@ +orderRepository = $orderRepository; + $this->evenManager = $evenManager; + } + + /** + * Handle the request + * + * @param array $handlingSubject + * @param array $response + * @throws LocalizedException + */ + public function handle(array $handlingSubject, array $response) + { + /** @var $order Order */ + $order = $handlingSubject['order']; + $order->registerCancellation('Payment Failed. Cancel Order.'); + + $this->evenManager->dispatch('order_cancel_after', ['order' => $order]); + $this->orderRepository->save($order); + } +} diff --git a/Gateway/Response/Handler/Capture/CaptureTransaction.php b/Gateway/Response/Handler/Capture/CaptureTransaction.php new file mode 100644 index 0000000..6064666 --- /dev/null +++ b/Gateway/Response/Handler/Capture/CaptureTransaction.php @@ -0,0 +1,97 @@ +logger = $logger; + $this->orderFactory = $orderFactory; + $this->transactionBuilder = $transactionBuilder; + $this->paymentResource = $paymentResource; + } + + /** + * Handle + * + * @param array $handlingSubject + * @param array $response + */ + public function handle(array $handlingSubject, array $response) + { + try { + $payment = $handlingSubject['payment']->getPayment(); + + if ($payment instanceof Payment) { + $chargeId = $response['id']; + $source = $response['source'] ?? []; + $order = $payment->getOrder(); + + $formattedPrice = $order->getBaseCurrency()->formatTxt( + $order->getGrandTotal() + ); + + $transaction = $this->transactionBuilder->setPayment($payment) + ->setOrder($order) + ->setTransactionId($chargeId) + ->setAdditionalInformation( + $payment->getTransactionAdditionalInfo() + ) + ->setFailSafe(true) + ->build(TransactionInterface::TYPE_CAPTURE); + + $payment->setCcLast4($source['card_masking'] ? substr($source['card_masking'], -4) : ''); + $payment->setCcAvsStatus('N/A'); + $payment->setAmountAuthorized($order->getGrandTotal()); + $payment->setCcNumberEnc($source['card_masking'] ?? ''); + $payment->addTransactionCommentsToOrder( + $transaction, + __('The Payment Capture success. Captured amount is %1.', $formattedPrice) + ); + $payment->setParentTransactionId(null); + } + } catch (\Exception $e) { + $this->logger->warning($e->getMessage()); + } + } +} diff --git a/Gateway/Response/Handler/Capture/OrderStatus.php b/Gateway/Response/Handler/Capture/OrderStatus.php new file mode 100644 index 0000000..19a7188 --- /dev/null +++ b/Gateway/Response/Handler/Capture/OrderStatus.php @@ -0,0 +1,54 @@ +logger = $logger; + $this->orderFactory = $orderFactory; + $this->orderRepository = $orderRepository; + } + + /** + * Handle + * + * @param array $handlingSubject + * @param array $response + */ + public function handle(array $handlingSubject, array $response) + { + $order = $handlingSubject['order']; + $order->addStatusToHistory($order->getStatus(), 'Payment Captured.'); + $this->orderRepository->save($order); + } +} diff --git a/Gateway/Response/Handler/ChargeIdHandler.php b/Gateway/Response/Handler/ChargeIdHandler.php new file mode 100644 index 0000000..890986c --- /dev/null +++ b/Gateway/Response/Handler/ChargeIdHandler.php @@ -0,0 +1,51 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment && isset($response['id']) && isset($response['object'])) { + if ($response['object'] === self::CHARGE_OBJECT) { + $order = $payment->getOrder(); + + $payment->setTransactionAdditionalInfo( + 'charge_id', + $response['id'] + ); + + $order->addStatusToHistory($order->getStatus(), 'Processed Charge Id: ' . $response['id']); + } + } + } +} diff --git a/Gateway/Response/Handler/CloseParentTransactionHandler.php b/Gateway/Response/Handler/CloseParentTransactionHandler.php new file mode 100644 index 0000000..d0ac810 --- /dev/null +++ b/Gateway/Response/Handler/CloseParentTransactionHandler.php @@ -0,0 +1,38 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/Gateway/Response/Handler/CloseTransactionHandler.php b/Gateway/Response/Handler/CloseTransactionHandler.php new file mode 100644 index 0000000..56d9475 --- /dev/null +++ b/Gateway/Response/Handler/CloseTransactionHandler.php @@ -0,0 +1,46 @@ +subjectReader = $subjectReader; + $this->closeTransaction = $closeTransaction; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $payment->setIsTransactionClosed($this->closeTransaction); + $payment->setShouldCloseParentTransaction(true); + } + } +} diff --git a/Gateway/Response/Handler/VoidResponseHandler.php b/Gateway/Response/Handler/VoidResponseHandler.php new file mode 100644 index 0000000..37c0f04 --- /dev/null +++ b/Gateway/Response/Handler/VoidResponseHandler.php @@ -0,0 +1,43 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function handle(array $handlingSubject, array $response) + { + $paymentDO = $this->subjectReader->readPayment($handlingSubject); + $payment = $paymentDO->getPayment(); + + if ($payment instanceof Payment) { + $chargeId = $response['id']; + + $payment->setIsTransactionClosed(true); + $payment->setShouldCloseParentTransaction(true); + $payment->setTransactionAdditionalInfo('real_transaction_id', $chargeId); + } + } +} diff --git a/Gateway/SubjectReader.php b/Gateway/SubjectReader.php new file mode 100644 index 0000000..c5e3f26 --- /dev/null +++ b/Gateway/SubjectReader.php @@ -0,0 +1,90 @@ +readPayment($subject) + ->getOrder() + ->getStoreId(); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } catch (\InvalidArgumentException $e) { + // No store id is current set + } + } + + return $storeId ? (int)$storeId : null; + } + + /** + * Reads amount from subject + * + * @param array $subject + * @return string + */ + public function readAmount(array $subject) + { + return (string)Helper\SubjectReader::readAmount($subject); + } + + /** + * Reads response from subject + * + * @param array $subject + * @return array + */ + public function readResponse(array $subject) + { + return Helper\SubjectReader::readResponse($subject); + } + + /** + * Reads login id from subject + * + * @param array $subject + * @return string|null + */ + public function readLoginId(array $subject) + { + return $subject['merchantAuthentication']['name'] ?? null; + } + + /** + * Reads transaction key from subject + * + * @param array $subject + * @return string|null + */ + public function readTransactionKey(array $subject) + { + return $subject['merchantAuthentication']['transactionKey'] ?? null; + } +} diff --git a/Gateway/Validator/GeneralResponseValidator.php b/Gateway/Validator/GeneralResponseValidator.php new file mode 100644 index 0000000..ee1e551 --- /dev/null +++ b/Gateway/Validator/GeneralResponseValidator.php @@ -0,0 +1,56 @@ +resultFactory = $resultFactory; + $this->subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject) + { + $response = $this->subjectReader->readResponse($validationSubject); + $object = $response['object']; + $isValid = ($object != 'error'); + $errorMessages = []; + + if (!$isValid) { + if (isset($response['code'])) { + $errorMessages[] = $response['message']; + } + } + + return $this->createResult($isValid, $errorMessages); + } +} diff --git a/Gateway/Validator/TransactionResponseValidator.php b/Gateway/Validator/TransactionResponseValidator.php new file mode 100644 index 0000000..51d9e4b --- /dev/null +++ b/Gateway/Validator/TransactionResponseValidator.php @@ -0,0 +1,79 @@ +subjectReader = $subjectReader; + } + + /** + * @inheritdoc + */ + public function validate(array $validationSubject) + { + $response = $this->subjectReader->readResponse($validationSubject); + + if ($this->isResponseCodeAnError($response)) { + $errorMessages = []; + + if (isset($response['failure_code'])) { + $errorMessages[] = $response['failure_message']; + } + + if ($response['object'] === 'error') { + $errorMessages[] = $response['message']; + } + + return $this->createResult(false, $errorMessages); + } + + return $this->createResult(true); + } + + /** + * Determines if the response code is actually an error + * + * @param array $transactionResponse + * @return bool + */ + private function isResponseCodeAnError(array $transactionResponse) + { + $object = $transactionResponse['object'] ?? false; + + if (!$object) { + return true; + } + + if ($object == 'error') { + return true; + } + + if (isset($transactionResponse['failure_code']) || isset($transactionResponse['failure_message'])) { + return true; + + } + + return false; + } +} diff --git a/Helper/Checkout.php b/Helper/Checkout.php new file mode 100644 index 0000000..391e79c --- /dev/null +++ b/Helper/Checkout.php @@ -0,0 +1,52 @@ +session = $session; + $this->urlBuilder = $urlBuilder; + } + + /** + * Cancel last placed order with specified comment message + * + * @param string $comment Comment appended to order history + * @return bool True if order cancelled, false otherwise + * @throws LocalizedException + */ + public function cancelCurrentOrder($comment): bool + { + $order = $this->session->getLastRealOrder(); + if ($order->getId() && $order->getState() != Order::STATE_CANCELED) { + $order->registerCancellation($comment)->save(); + return true; + } + return false; + } +} diff --git a/Logger/Handler.php b/Logger/Handler.php new file mode 100644 index 0000000..d466f84 --- /dev/null +++ b/Logger/Handler.php @@ -0,0 +1,18 @@ +getConfig()->isDebugMode() && parent::isHandling($level); + } + + /** + * Get configuration + * + * @return Config + */ + public function getConfig() + { + if ($this->config === null) { + $this->config = ObjectManager::getInstance()->get(Config::class); + } + + return $this->config; + } + + /** + * This method allows for compatibility with common interfaces. + * + * @param string|array $message + * @param array $context + * @return void + */ + public function debug($message, array $context = []): void + { + if (is_array($message)) { + $message = var_export($message, true); + } + + parent::debug($message, $context); + } + + /** + * This method allows for compatibility with common interfaces. + * + * @param string|array $message + * @param array $context + * @return void + */ + public function info($message, array $context = []): void + { + if (is_array($message)) { + $message = var_export($message, true); + } + + parent::info($message, $context); + } +} diff --git a/Model/Adminhtml/Source/Cctype.php b/Model/Adminhtml/Source/Cctype.php new file mode 100644 index 0000000..4aa98d4 --- /dev/null +++ b/Model/Adminhtml/Source/Cctype.php @@ -0,0 +1,15 @@ + self::ENVIRONMENT_SANDBOX, + 'label' => 'Sandbox', + ], + [ + 'value' => self::ENVIRONMENT_PRODUCTION, + 'label' => 'Production' + ] + ]; + } +} diff --git a/Model/Adminhtml/Source/PaymentAction.php b/Model/Adminhtml/Source/PaymentAction.php new file mode 100644 index 0000000..aa4a399 --- /dev/null +++ b/Model/Adminhtml/Source/PaymentAction.php @@ -0,0 +1,24 @@ + 'authorize', + 'label' => __('Authorize Only'), + ], + [ + 'value' => 'authorize_capture', + 'label' => __('Authorize and Capture') + ] + ]; + } +} diff --git a/Model/Config/Backend/SmartPayId.php b/Model/Config/Backend/SmartPayId.php new file mode 100644 index 0000000..5856ebe --- /dev/null +++ b/Model/Config/Backend/SmartPayId.php @@ -0,0 +1,21 @@ +getValue(); + $this->setValue($data); + + return parent::beforeSave(); + } +} diff --git a/Model/Data/Mpi.php b/Model/Data/Mpi.php new file mode 100644 index 0000000..64b5d46 --- /dev/null +++ b/Model/Data/Mpi.php @@ -0,0 +1,73 @@ +_get(self::ECI); + } + + /** + * @inheritDoc + */ + public function setEci($eci) + { + return $this->setData(self::ECI, $eci); + } + + /** + * @inheritDoc + */ + public function getXid() + { + return $this->_get(self::XID); + } + + /** + * @inheritDoc + */ + public function setXid($xid) + { + return $this->setData(self::XID, $xid); + } + + /** + * @inheritDoc + */ + public function getCavv() + { + return $this->_get(self::CAVV); + } + + /** + * @inheritDoc + */ + public function setCavv($cavv) + { + return $this->setData(self::CAVV, $cavv); + } + + /** + * @inheritDoc + */ + public function getKbankMpi() + { + return $this->_get(self::KBANK_MPI); + } + + /** + * @inheritDoc + */ + public function setKbankMpi($kbankMpi) + { + return $this->setData(self::KBANK_MPI, $kbankMpi); + } +} diff --git a/Model/Data/Notify.php b/Model/Data/Notify.php new file mode 100644 index 0000000..1f48cab --- /dev/null +++ b/Model/Data/Notify.php @@ -0,0 +1,394 @@ +_get(self::ID); + } + + /** + * @inheritDoc + */ + public function setId($id) + { + return $this->setData(self::ID, $id); + } + + /** + * @inheritDoc + */ + public function getObject() + { + return $this->_get(self::OBJECT); + } + + /** + * @inheritDoc + */ + public function setObject($object) + { + return $this->setData(self::OBJECT, $object); + } + + /** + * @inheritDoc + */ + public function getAmount() + { + return $this->_get(self::AMOUNT); + } + + /** + * @inheritDoc + */ + public function setAmount($amount) + { + return $this->setData(self::AMOUNT, $amount); + } + + /** + * @inheritDoc + */ + public function getCurrency() + { + return $this->_get(self::CURRENCY); + } + + /** + * @inheritDoc + */ + public function setCurrency($currency) + { + return $this->setData(self::CURRENCY, $currency); + } + + /** + * @inheritDoc + */ + public function getTransactionState() + { + return $this->_get(self::TRANSACTION_STATE); + } + + /** + * @inheritDoc + */ + public function setTransactionState($transactionState) + { + return $this->setData(self::TRANSACTION_STATE, $transactionState); + } + + /** + * @inheritDoc + */ + public function getSource() + { + return $this->_get(self::SOURCE); + } + + /** + * @inheritDoc + */ + public function setSource($source) + { + return $this->setData(self::SOURCE, $source); + } + + /** + * @inheritDoc + */ + public function getCreated() + { + return $this->_get(self::CREATED); + } + + /** + * @inheritDoc + */ + public function setCreated($created) + { + return $this->setData(self::CREATED, $created); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->_get(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus($status) + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getLivemode() + { + return $this->_get(self::LIVEMODE); + } + + /** + * @inheritDoc + */ + public function setLivemode($livemode) + { + return $this->setData(self::LIVEMODE, $livemode); + } + + /** + * @inheritDoc + */ + public function getMetadata() + { + return $this->_get(self::METADATA); + } + + /** + * @inheritDoc + */ + public function setMetadata($metadata) + { + return $this->setData(self::METADATA, $metadata); + } + + /** + * @inheritDoc + */ + public function getFailureCode() + { + return $this->_get(self::FAILURE_CODE); + } + + /** + * @inheritDoc + */ + public function setFailureCode($failureCode) + { + return $this->setData(self::FAILURE_CODE, $failureCode); + } + + /** + * @inheritDoc + */ + public function getFailureMessage() + { + return $this->_get(self::FAILURE_MESSAGE); + } + + /** + * @inheritDoc + */ + public function setFailureMessage($failureMessage) + { + return $this->setData(self::FAILURE_MESSAGE, $failureMessage); + } + + /** + * @inheritDoc + */ + public function getDescription() + { + return $this->_get(self::DESCRIPTION); + } + + /** + * @inheritDoc + */ + public function setDescription($description) + { + return $this->setData(self::DESCRIPTION, $description); + } + + /** + * @inheritDoc + */ + public function getMpi() + { + return $this->_get(self::MPI); + } + + /** + * @inheritDoc + */ + public function setMpi($mpi) + { + return $this->setData(self::MPI, $mpi); + } + + /** + * @inheritDoc + */ + public function getChecksum() + { + return $this->_get(self::CHECKSUM); + } + + /** + * @inheritDoc + */ + public function setChecksum($checksum) + { + return $this->setData(self::CHECKSUM, $checksum); + } + + /** + * @inheritDoc + */ + public function getRedirectUrl() + { + return $this->_get(self::REDIRECT_URL); + } + + /** + * @inheritDoc + */ + public function setRedirectUrl($redirectUrl) + { + return $this->setData(self::REDIRECT_URL, $redirectUrl); + } + + /** + * @inheritDoc + */ + public function getSettlementInfo() + { + return $this->_get(self::SETTLEMENT_INFO); + } + + /** + * @inheritDoc + */ + public function setSettlementInfo($settlementInfo) + { + return $this->setData(self::SETTLEMENT_INFO, $settlementInfo); + } + + /** + * @inheritDoc + */ + public function getRefundInfo() + { + return $this->_get(self::REFUND_INFO); + } + + /** + * @inheritDoc + */ + public function setRefundInfo($refundInfo) + { + return $this->setData(self::REFUND_INFO, $refundInfo); + } + + /** + * @inheritDoc + */ + public function getApprovalCode() + { + return $this->_get(self::APPROVAL_CODE); + } + + /** + * @inheritDoc + */ + public function setApprovalCode($approvalCode) + { + return $this->setData(self::APPROVAL_CODE, $approvalCode); + } + + /** + * @inheritDoc + */ + public function getCampaignId() + { + return $this->_get(self::CAMPAIGN_ID); + } + + /** + * @inheritDoc + */ + public function setCampaignId($campaignId) + { + return $this->setData(self::CAMPAIGN_ID, $campaignId); + } + + /** + * @inheritDoc + */ + public function getRef1() + { + return $this->_get(self::REF_1); + } + + /** + * @inheritDoc + */ + public function setRef1($ref1) + { + return $this->setData(self::REF_1, $ref1); + } + + /** + * @inheritDoc + */ + public function getRef2() + { + return $this->_get(self::REF_2); + } + + /** + * @inheritDoc + */ + public function setRef2($ref2) + { + return $this->setData(self::REF_2, $ref2); + } + + /** + * @inheritDoc + */ + public function getRef3() + { + return $this->_get(self::REF_3); + } + + /** + * @inheritDoc + */ + public function setRef3($ref3) + { + return $this->setData(self::REF_3, $ref3); + } + + /** + * @inheritDoc + */ + public function getReferenceOrder() + { + return $this->_get(self::REFERENCE_ORDER); + } + + /** + * @inheritDoc + */ + public function setReferenceOrder($referenceOrder) + { + return $this->setData(self::REFERENCE_ORDER, $referenceOrder); + } +} diff --git a/Model/Data/Source.php b/Model/Data/Source.php new file mode 100644 index 0000000..3d89d5c --- /dev/null +++ b/Model/Data/Source.php @@ -0,0 +1,90 @@ +_get(self::ID); + } + + /** + * @inheritDoc + */ + public function setId($id) + { + return $this->setData(self::ID, $id); + } + + /** + * @inheritDoc + */ + public function getObject() + { + return $this->_get(self::OBJECT); + } + + /** + * @inheritDoc + */ + public function setObject($object) + { + return $this->setData(self::OBJECT, $object); + } + + /** + * @inheritDoc + */ + public function getBrand() + { + return $this->_get(self::BRAND); + } + + /** + * @inheritDoc + */ + public function setBrand($brand) + { + return $this->setData(self::BRAND, $brand); + } + + /** + * @inheritDoc + */ + public function getCardMasking() + { + return $this->_get(self::CARD_MASKING); + } + + /** + * @inheritDoc + */ + public function setCardMasking($cardMasking) + { + return $this->setData(self::CARD_MASKING, $cardMasking); + } + + /** + * @inheritDoc + */ + public function getIssuerBank() + { + return $this->_get(self::ISSUER_BANK); + } + + /** + * @inheritDoc + */ + public function setIssuerBank($issuerBank) + { + return $this->setData(self::ISSUER_BANK, $issuerBank); + } +} diff --git a/Model/InstallmentManagement.php b/Model/InstallmentManagement.php new file mode 100644 index 0000000..a7acdd5 --- /dev/null +++ b/Model/InstallmentManagement.php @@ -0,0 +1,69 @@ +config = $config; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getAvailableInstallment(string $methodCode, ?int $storeId = null): array + { + $availableInstallmentsStripped = []; + $availableInstallments = $this->config->getSpecificPaymentConfig($methodCode, Config::KEY_INSTALLMENT_INFO); + + if (!$availableInstallments) { + return $availableInstallmentsStripped; + } + + try { + $configurations = $this->serializer->unserialize($availableInstallments); + $i = 0; + if (is_array($configurations)) { + foreach ($configurations as $configuration) { + if (!$configuration['status']) { + continue; + } + $availableInstallmentsStripped[$i]['smartpay_id'] = preg_replace( + '/\s+/', + '', + $configuration['smartpay_id'] + ); + $availableInstallmentsStripped[$i]['payment_term'] = preg_replace( + '/\s+/', + '', + $configuration['payment_term'] + ); + $availableInstallmentsStripped[$i]['installment_title'] = trim($configuration['installment_title']); + $i++; + } + } + } catch (\Exception $e) { //phpcs:ignore Magento2.CodeAnalysis.EmptyBlock + } finally { + return $availableInstallmentsStripped; + } + } +} diff --git a/Model/Meta.php b/Model/Meta.php new file mode 100644 index 0000000..fa87077 --- /dev/null +++ b/Model/Meta.php @@ -0,0 +1,498 @@ +_init(ResourceMeta::class); + } + + /** + * @inheritdoc + */ + public function getMetaId() + { + return $this->getData(self::META_ID); + } + + /** + * @inheritdoc + */ + public function setMetaId(int $metaId) + { + return $this->setData(self::META_ID, $metaId); + } + + /** + * @inheritdoc + */ + public function getOrderId() + { + return $this->getData(self::ORDER_ID); + } + + /** + * @inheritdoc + */ + public function setOrderId(string $orderId) + { + return $this->setData(self::ORDER_ID, $orderId); + } + + /** + * @inheritdoc + */ + public function getCustomerId() + { + return $this->getData(self::CUSTOMER_ID); + } + + /** + * @inheritdoc + */ + public function setCustomerId($customerId) + { + return $this->setData(self::CUSTOMER_ID, $customerId); + } + + /** + * @inheritdoc + */ + public function getChargeId() + { + return $this->getData(self::CHARGE_ID); + } + + /** + * @inheritdoc + */ + public function setChargeId($chargeId) + { + return $this->setData(self::CHARGE_ID, $chargeId); + } + + /** + * @inheritdoc + */ + public function getObject() + { + return $this->getData(self::OBJECT); + } + + /** + * @inheritdoc + */ + public function setObject($object) + { + return $this->setData(self::OBJECT, $object); + } + + /** + * @inheritdoc + */ + public function getAmount() + { + return $this->getData(self::AMOUNT); + } + + /** + * @inheritdoc + */ + public function setAmount($amount) + { + return $this->setData(self::AMOUNT, $amount); + } + + /** + * @inheritdoc + */ + public function getCurrency() + { + return $this->getData(self::CURRENCY); + } + + /** + * @inheritdoc + */ + public function setCurrency($currency) + { + return $this->setData(self::CURRENCY, $currency); + } + + /** + * @inheritDoc + */ + public function getTransactionState() + { + return $this->getData(self::TRANSACTION_STATE); + } + + /** + * @inheritDoc + */ + public function setTransactionState($transactionState) + { + return $this->setData(self::TRANSACTION_STATE, $transactionState); + } + + /** + * @inheritDoc + */ + public function getCreated() + { + return $this->getData(self::CREATED); + } + + /** + * @inheritDoc + */ + public function setCreated($created) + { + return $this->setData(self::CREATED, $created); + } + + /** + * @inheritDoc + */ + public function getStatus() + { + return $this->getData(self::STATUS); + } + + /** + * @inheritDoc + */ + public function setStatus($status) + { + return $this->setData(self::STATUS, $status); + } + + /** + * @inheritDoc + */ + public function getApprovalCode() + { + return $this->getData(self::APPROVAL_CODE); + } + + /** + * @inheritDoc + */ + public function setApprovalCode($approvalCode) + { + return $this->setData(self::APPROVAL_CODE, $approvalCode); + } + + /** + * @inheritDoc + */ + public function getLivemode() + { + return $this->getData(self::LIVEMODE); + } + + /** + * @inheritDoc + */ + public function setLivemode($livemode) + { + return $this->setData(self::LIVEMODE, $livemode); + } + + /** + * @inheritDoc + */ + public function getMetadata() + { + return $this->getData(self::METADATA); + } + + /** + * @inheritDoc + */ + public function setMetadata($metadata) + { + return $this->setData(self::METADATA, $metadata); + } + + /** + * @inheritDoc + */ + public function getFailureCode() + { + return $this->getData(self::FAILURE_CODE); + } + + /** + * @inheritDoc + */ + public function setFailureCode($failureCode) + { + return $this->setData(self::FAILURE_CODE, $failureCode); + } + + /** + * @inheritDoc + */ + public function getFailureMessage() + { + return $this->getData(self::FAILURE_MESSAGE); + } + + /** + * @inheritDoc + */ + public function setFailureMessage($failureMessage) + { + return $this->setData(self::FAILURE_MESSAGE, $failureMessage); + } + + /** + * @inheritDoc + */ + public function getRedirectUrl() + { + return $this->getData(self::REDIRECT_URL); + } + + /** + * @inheritDoc + */ + public function setRedirectUrl($redirectUrl) + { + return $this->setData(self::REDIRECT_URL, $redirectUrl); + } + + /** + * @inheritDoc + */ + public function getCardId() + { + return $this->getData(self::CARD_ID); + } + + /** + * @inheritDoc + */ + public function setCardId($cardId) + { + return $this->setData(self::CARD_ID, $cardId); + } + + /** + * @inheritDoc + */ + public function getCardBrand() + { + return $this->getData(self::CARD_BRAND); + } + + /** + * @inheritDoc + */ + public function setCardBrand($cardBrand) + { + return $this->setData(self::CARD_BRAND, $cardBrand); + } + + /** + * @inheritDoc + */ + public function getIssuerBank() + { + return $this->getData(self::ISSUER_BANK); + } + + /** + * @inheritDoc + */ + public function setIssuerBank($issuerBank) + { + return $this->setData(self::ISSUER_BANK, $issuerBank); + } + + /** + * @inheritDoc + */ + public function getMaskedPan() + { + return $this->getData(self::MASKED_PAN); + } + + /** + * @inheritDoc + */ + public function setMaskedPan($maskedPan) + { + return $this->setData(self::MASKED_PAN, $maskedPan); + } + + /** + * @inheritDoc + */ + public function getRef1() + { + return $this->getData(self::REF1); + } + + /** + * @inheritDoc + */ + public function setRef1($ref1) + { + return $this->setData(self::REF1, $ref1); + } + + /** + * @inheritDoc + */ + public function getRef2() + { + return $this->getData(self::REF2); + } + + /** + * @inheritDoc + */ + public function setRef2($ref2) + { + return $this->setData(self::REF2, $ref2); + } + + /** + * @inheritDoc + */ + public function getRef3() + { + return $this->getData(self::REF3); + } + + /** + * @inheritDoc + */ + public function setRef3($ref3) + { + return $this->setData(self::REF3, $ref3); + } + + /** + * @inheritDoc + */ + public function getBahtAmount() + { + return $this->getData(self::BAHT_AMOUNT); + } + + /** + * @inheritDoc + */ + public function setBahtAmount($bahtAmount) + { + return $this->setData(self::BAHT_AMOUNT, $bahtAmount); + } + + /** + * @inheritDoc + */ + public function getRate() + { + return $this->getData(self::RATE); + } + + /** + * @inheritDoc + */ + public function setRate($rate) + { + return $this->setData(self::RATE, $rate); + } + + /** + * @inheritDoc + */ + public function getConvertedAmount() + { + return $this->getData(self::CONVERTED_AMOUNT); + } + + /** + * @inheritDoc + */ + public function setConvertedAmount($convertedAmount) + { + return $this->setData(self::CONVERTED_AMOUNT, $convertedAmount); + } + + /** + * @inheritDoc + */ + public function getConvertedCurrencyCode() + { + return $this->getData(self::CONVERTED_CURRENCY_CODE); + } + + /** + * @inheritDoc + */ + public function setConvertedCurrencyCode($convertedCurrencyCode) + { + return $this->setData(self::CONVERTED_CURRENCY_CODE, $convertedCurrencyCode); + } + + /** + * @inheritDoc + */ + public function getConvertedCurrencyName() + { + return $this->getData(self::CONVERTED_CURRENCY_NAME); + } + + /** + * @inheritDoc + */ + public function setConvertedCurrencyName($convertedCurrencyName) + { + return $this->setData(self::CONVERTED_CURRENCY_NAME, $convertedCurrencyName); + } + + /** + * @inheritDoc + */ + public function getF63() + { + return $this->getData(self::F63); + } + + /** + * @inheritDoc + */ + public function setF63($f63) + { + return $this->setData(self::F63, $f63); + } + + /** + * @inheritDoc + */ + public function getMid() + { + return $this->getData(self::MID); + } + + /** + * @inheritDoc + */ + public function setMid($mid) + { + return $this->setData(self::MID, $mid); + } +} diff --git a/Model/MetaRepository.php b/Model/MetaRepository.php new file mode 100644 index 0000000..ab4b982 --- /dev/null +++ b/Model/MetaRepository.php @@ -0,0 +1,210 @@ +resource = $resource; + $this->metaFactory = $metaFactory; + $this->metaCollectionFactory = $metaCollectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->dataMetaFactory = $dataMetaFactory; + $this->dataObjectProcessor = $dataObjectProcessor; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function save( + \GhoSter\KbankPayments\Api\Data\MetaInterface $meta + ) { + try { + $this->resource->save($meta); + } catch (\Exception $exception) { + throw new CouldNotSaveException(__( + 'Could not save the meta: %1', + $exception->getMessage() + )); + } + return $meta; + } + + /** + * @inheritdoc + */ + public function getById($metaId) + { + $meta = $this->metaFactory->create(); + $this->resource->load($meta, $metaId); + if (!$meta->getId()) { + throw new NoSuchEntityException(__('Meta with id "%1" does not exist.', $metaId)); + } + return $meta; + } + + /** + * @inheritdoc + */ + public function getByOrderIncrement($orderIncrementId) + { + $meta = $this->metaFactory->create(); + $this->resource->loadByOrderIncrement($meta, $orderIncrementId); + return $meta; + } + + /** + * @inheritdoc + */ + public function getByChargeId($chargeId) + { + $meta = $this->metaFactory->create(); + $this->resource->loadByChargeId($meta, $chargeId); + if (!$meta->getId()) { + throw new NoSuchEntityException(__('Meta with charge id %1 does not exist.', $chargeId)); + } + return $meta; + } + + /** + * @inheritdoc + */ + public function getList( + SearchCriteriaInterface $searchCriteria + ) { + $collection = $this->metaCollectionFactory->create(); + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + $fields = []; + $conditions = []; + foreach ($filterGroup->getFilters() as $filter) { + if ($filter->getField() === 'store_id') { + $collection->addStoreFilter($filter->getValue(), false); + continue; + } + $fields[] = $filter->getField(); + $condition = $filter->getConditionType() ?: 'eq'; + $conditions[] = [$condition => $filter->getValue()]; + } + $collection->addFieldToFilter($fields, $conditions); + } + + $sortOrders = $searchCriteria->getSortOrders(); + if ($sortOrders) { + /** @var SortOrder $sortOrder */ + foreach ($sortOrders as $sortOrder) { + $collection->addOrder( + $sortOrder->getField(), + ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' + ); + } + } + $collection->setCurPage($searchCriteria->getCurrentPage()); + $collection->setPageSize($searchCriteria->getPageSize()); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setTotalCount($collection->getSize()); + $searchResults->setItems($collection->getItems()); + return $searchResults; + } + + /** + * @inheritdoc + */ + public function delete( + \GhoSter\KbankPayments\Api\Data\MetaInterface $meta + ) { + try { + $this->resource->delete($meta); + } catch (\Exception $exception) { + throw new CouldNotDeleteException(__( + 'Could not delete the Meta: %1', + $exception->getMessage() + )); + } + return true; + } + + /** + * @inheritdoc + */ + public function deleteById($metaId) + { + return $this->delete($this->getById($metaId)); + } +} diff --git a/Model/NotifyManagement.php b/Model/NotifyManagement.php new file mode 100644 index 0000000..8e95d69 --- /dev/null +++ b/Model/NotifyManagement.php @@ -0,0 +1,261 @@ +request = $request; + $this->metaRepository = $metaRepository; + $this->kbankEmbeddedConfig = $config; + $this->orderFactory = $orderFactory; + $this->dataObjectFactory = $dataObjectFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->eventManager = $eventManager; + $this->notifyDataFactory = $notifyDataFactory; + $this->sourceDataFactory = $sourceDataFactory; + $this->mpiDataFactory = $mpiDataFactory; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function notify(): bool + { + $notifyData = $this->request->getBodyParams(); + + /** @var SourceInterface $mpi */ + $source = $this->sourceDataFactory->create(); + $this->dataObjectHelper->populateWithArray( + $source, + $notifyData['source'] ?? [], + SourceInterface::class + ); + + /** @var MpiInterface $mpi */ + $mpi = $this->mpiDataFactory->create(); + $this->dataObjectHelper->populateWithArray( + $mpi, + $notifyData['mpi'] ?? [], + MpiInterface::class + ); + + /** @var NotifyInterface $notify */ + $notify = $this->notifyDataFactory->create(); + + $this->dataObjectHelper->populateWithArray( + $notify, + $notifyData, + NotifyInterface::class + ); + + $notify->setSource($source) + ->setMpi($mpi); + + if ($notify->getStatus() !== Config::VALID_STATUS) { + $this->logger->info(sprintf('Status was mismatched or empty. %s', $notify->getStatus())); + return false; + } + + if (!$notify->getId()) { + $this->logger->info(sprintf('Charge Id was mismatched or empty. %s', $notify->getId())); + return false; + } + + if (!$notify->getChecksum()) { + $this->logger->info(sprintf('Checksum was mismatched or empty. %s', $notify->getChecksum())); + return false; + } + + $concatenate = $notify->getId() + . number_format($notify->getAmount(), 4, '.', '') + . $notify->getCurrency() + . $notify->getStatus() + . $notify->getTransactionState(); + + $salt = $this->kbankEmbeddedConfig->getSecretKey(); + $expectedCheckSum = hash('sha256', $concatenate . $salt); + + if ($notify->getChecksum() !== $expectedCheckSum) { + $this->logger->info(sprintf('Checksum was mismatched or empty. %s', $notify->getChecksum())); + return false; + } + + try { + $meta = $this->metaRepository->getByChargeId($notify->getId()); + + if (!$meta->getMetaId()) { + $this->logger->info('Meta was mismatched or empty.'); + return false; + } + + /** @var Order $order */ + $order = $this->orderFactory->create() + ->loadByIncrementId($meta->getOrderId()); + + if (!$order->getId() || $order->getIncrementId() !== $notify->getReferenceOrder()) { + $this->logger->info(sprintf('Order was mismatched or empty. %s', $notify->getReferenceOrder())); + return false; + } + + if ($this->_checkOrderState($order)) { + $this->logger->info(sprintf('Order status already changed. %s', $order->getStatus())); + } else { + $errorObject = $this->dataObjectFactory + ->create(['data' => ['error' => $this->errors]]); + + $this->eventManager->dispatch( + 'kbank_embedded_payment_method_callback_validation', + [ + 'meta' => $meta, + 'order' => $order, + 'error' => $errorObject, + 'type' => 'notify' + ] + ); + + if (!empty($errorObject->getData('error')) && is_array($errorObject->getData('error'))) { + $comment = implode(PHP_EOL, $errorObject->getData('error')); + $this->logger->critical("Something went wrong while validation\n"); + $this->logger->critical($comment); + + $order->cancel(); + $order->save(); + return false; + } + } + } catch (\Exception $e) { + $this->logger->critical("Something went wrong while notify\n" . $e->getMessage()); + return false; + } + + return true; + } + + /** + * Check order state + * + * @param Order $order + * @return bool + */ + protected function _checkOrderState(Order $order): bool + { + if (!$order->getEntityId()) { + return false; + } + + return in_array($order->getState(), $this->allowedOrderStates); + } +} diff --git a/Model/OrderInformationManagement.php b/Model/OrderInformationManagement.php new file mode 100644 index 0000000..b5c61a5 --- /dev/null +++ b/Model/OrderInformationManagement.php @@ -0,0 +1,45 @@ +orderRepository = $orderRepository; + } + + /** + * @inheritDoc + */ + public function validateOrderStatus(int $orderId): bool + { + try { + $order = $this->orderRepository->get($orderId); + return in_array($order->getState(), $this->allowedOrderStates); + } catch (\Exception $e) { + return false; + } + } +} diff --git a/Model/PaymentStatus.php b/Model/PaymentStatus.php new file mode 100644 index 0000000..26b1292 --- /dev/null +++ b/Model/PaymentStatus.php @@ -0,0 +1,38 @@ +_init('ghoster_kbank_meta', 'meta_id'); + $this->_isPkAutoIncrement = false; + } + + /** + * Load by Order Increment Id + * + * @param \GhoSter\KbankPayments\Model\Meta $meta + * @param string $orderIncrementId + * @return $this + * @throws LocalizedException + */ + public function loadByOrderIncrement(\GhoSter\KbankPayments\Model\Meta $meta, $orderIncrementId) + { + $connection = $this->getConnection(); + $bind = ['order_id' => $orderIncrementId]; + $select = $connection->select() + ->from($this->getMainTable(), $this->getIdFieldName()) + ->where( + 'order_id = :order_id' + )->order('created DESC'); + + $metaId = $connection->fetchOne($select, $bind); + + if ($metaId) { + $this->load($meta, $metaId); + } else { + $meta->setData([]); + } + + return $this; + } + + /** + * Load by Charge Id + * + * @param \GhoSter\KbankPayments\Model\Meta $meta + * @param mixed $chargeId + * @return $this + * @throws LocalizedException + */ + public function loadByChargeId(\GhoSter\KbankPayments\Model\Meta $meta, $chargeId): Meta + { + $connection = $this->getConnection(); + $bind = ['charge_id' => $chargeId]; + $select = $connection->select() + ->from($this->getMainTable(), $this->getIdFieldName()) + ->where( + 'charge_id = :charge_id' + ); + + $metaId = $connection->fetchOne($select, $bind); + + if ($metaId) { + $this->load($meta, $metaId); + } else { + $meta->setData([]); + } + + return $this; + } +} diff --git a/Model/ResourceModel/Meta/Collection.php b/Model/ResourceModel/Meta/Collection.php new file mode 100644 index 0000000..d0c8720 --- /dev/null +++ b/Model/ResourceModel/Meta/Collection.php @@ -0,0 +1,23 @@ +_init( + Meta::class, + ResourceMeta::class + ); + } +} diff --git a/Model/ResourceModel/Token.php b/Model/ResourceModel/Token.php new file mode 100644 index 0000000..0b296c6 --- /dev/null +++ b/Model/ResourceModel/Token.php @@ -0,0 +1,75 @@ +_init('ghoster_kbank_token', 'token_id'); + } + + /** + * Load by Order Increment Id + * + * @param \GhoSter\KbankPayments\Model\Token $token + * @param string $orderIncrementId + * @return $this + * @throws LocalizedException + */ + public function loadByOrderIncrement(\GhoSter\KbankPayments\Model\Token $token, string $orderIncrementId): Token + { + $connection = $this->getConnection(); + $bind = ['order_id' => $orderIncrementId]; + $select = $connection->select() + ->from($this->getMainTable(), $this->getIdFieldName()) + ->where( + 'order_id = :order_id' + )->order('created DESC'); + + $tokenId = $connection->fetchOne($select, $bind); + + if ($tokenId) { + $this->load($token, $tokenId); + } else { + $token->setData([]); + } + + return $this; + } + + /** + * Load by Token + * + * @param \GhoSter\KbankPayments\Model\Token $token + * @param string $tokenValue + * @return $this + * @throws LocalizedException + */ + public function loadByToken(\GhoSter\KbankPayments\Model\Token $token, string $tokenValue): Token + { + $connection = $this->getConnection(); + $bind = ['token' => $tokenValue]; + $select = $connection->select() + ->from($this->getMainTable(), $this->getIdFieldName()) + ->where( + 'token = :token' + ); + + $tokenId = $connection->fetchOne($select, $bind); + + if ($tokenId) { + $this->load($token, $tokenId); + } else { + $token->setData([]); + } + + return $this; + } +} diff --git a/Model/ResourceModel/Token/Collection.php b/Model/ResourceModel/Token/Collection.php new file mode 100644 index 0000000..2cf7697 --- /dev/null +++ b/Model/ResourceModel/Token/Collection.php @@ -0,0 +1,21 @@ +_init( + \GhoSter\KbankPayments\Model\Token::class, + \GhoSter\KbankPayments\Model\ResourceModel\Token::class + ); + } +} diff --git a/Model/Token.php b/Model/Token.php new file mode 100644 index 0000000..dd5f00e --- /dev/null +++ b/Model/Token.php @@ -0,0 +1,242 @@ +_init(\GhoSter\KbankPayments\Model\ResourceModel\Token::class); + } + + /** + * @inheritDoc + */ + public function getTokenId() + { + return $this->getData(self::TOKEN_ID); + } + + /** + * @inheritDoc + */ + public function setTokenId($tokenId) + { + return $this->setData(self::TOKEN_ID, $tokenId); + } + + /** + * @inheritDoc + */ + public function getToken() + { + return $this->getData(self::TOKEN); + } + + /** + * @inheritDoc + */ + public function setToken($token) + { + return $this->setData(self::TOKEN, $token); + } + + /** + * @inheritDoc + */ + public function getOrderId() + { + return $this->getData(self::ORDER_ID); + } + + /** + * @inheritDoc + */ + public function setOrderId($orderId) + { + return $this->setData(self::ORDER_ID, $orderId); + } + + /** + * @inheritDoc + */ + public function getCustomerId() + { + return $this->getData(self::CUSTOMER_ID); + } + + /** + * @inheritDoc + */ + public function setCustomerId($customerId) + { + return $this->setData(self::CUSTOMER_ID, $customerId); + } + + /** + * @inheritDoc + */ + public function getPaymentMethods() + { + return $this->getData(self::PAYMENT_METHODS); + } + + /** + * @inheritDoc + */ + public function setPaymentMethods($paymentMethods) + { + return $this->setData(self::PAYMENT_METHODS, $paymentMethods); + } + + /** + * @inheritDoc + */ + public function getCreatedTime() + { + return $this->getData(self::CREATED_TIME); + } + + /** + * @inheritDoc + */ + public function setCreatedTime($createdTime) + { + return $this->setData(self::CREATED_TIME, $createdTime); + } + + /** + * @inheritDoc + */ + public function getDccCurrency() + { + return $this->getData(self::DCC_CURRENCY); + } + + /** + * @inheritDoc + */ + public function setDccCurrency($dccCurrency) + { + return $this->setData(self::DCC_CURRENCY, $dccCurrency); + } + + /** + * @inheritDoc + */ + public function getSmartpayId() + { + return $this->getData(self::SMARTPAY_ID); + } + + /** + * @inheritDoc + */ + public function setSmartpayId($smartpayId) + { + return $this->setData(self::SMARTPAY_ID, $smartpayId); + } + + /** + * @inheritDoc + */ + public function getTerm() + { + return $this->getData(self::TERM); + } + + /** + * @inheritDoc + */ + public function setTerm($term) + { + return $this->setData(self::TERM, $term); + } + + /** + * @inheritDoc + */ + public function getSaveCard() + { + return $this->getData(self::SAVECARD); + } + + /** + * @inheritDoc + */ + public function setSaveCard($saveCard) + { + return $this->setData(self::SAVECARD, $saveCard); + } + + /** + * @inheritDoc + */ + public function getCardId() + { + return $this->getData(self::CARDID); + } + + /** + * @inheritDoc + */ + public function setCardId($cardId) + { + return $this->setData(self::CARDID, $cardId); + } + + /** + * @inheritDoc + */ + public function getMid() + { + return $this->getData(self::MID); + } + + /** + * @inheritDoc + */ + public function setMid($mid) + { + return $this->setData(self::MID, $mid); + } + + /** + * @inheritDoc + */ + public function getTerminalId() + { + return $this->getData(self::TERMINAL_ID); + } + + /** + * @inheritDoc + */ + public function setTerminalId($terminalId) + { + return $this->setData(self::TERMINAL_ID, $terminalId); + } + + /** + * @inheritDoc + */ + public function getExtensionAttributes() + { + return $this->_getExtensionAttributes(); + } + + /** + * @inheritDoc + */ + public function setExtensionAttributes( + \GhoSter\KbankPayments\Api\Data\TokenExtensionInterface $extensionAttributes = null + ) { + return $this->_setExtensionAttributes($extensionAttributes); + } +} diff --git a/Model/TokenManagement.php b/Model/TokenManagement.php new file mode 100644 index 0000000..fad3c97 --- /dev/null +++ b/Model/TokenManagement.php @@ -0,0 +1,85 @@ +tokenRepository = $tokenRepository; + $this->transactionProcessor = $transactionProcessor; + $this->orderFactory = $orderFactory; + $this->metaFactory = $metaFactory; + } + + /** + * @inheritDoc + */ + public function saveTokenForOrder(TokenInterface $token, string $orderIncrement): TokenInterface + { + $token->setOrderId($orderIncrement); + + if (!$token->getTerminalId()) { + $token->setTerminalId($this->config->getTerminalId()); + } + + return $this->tokenRepository->save($token); + } + + /** + * @inheritDoc + */ + public function saveTokenThenAuthorize(TokenInterface $token, string $orderIncrement): MetaInterface + { + /** @var Meta $meta */ + $meta = $this->metaFactory->create(); + + $token = $this->saveTokenForOrder($token, $orderIncrement); + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrement); + + if ($token->getTokenId() && $order->getEntityId()) { + return $this->transactionProcessor->authorizeOrderByToken($order, $token); + } + + return $meta; + } +} diff --git a/Model/TokenRepository.php b/Model/TokenRepository.php new file mode 100644 index 0000000..d8594c6 --- /dev/null +++ b/Model/TokenRepository.php @@ -0,0 +1,217 @@ +resource = $resource; + $this->tokenFactory = $tokenFactory; + $this->tokenCollectionFactory = $tokenCollectionFactory; + $this->searchResultsFactory = $searchResultsFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->dataTokenFactory = $dataTokenFactory; + $this->dataObjectProcessor = $dataObjectProcessor; + $this->storeManager = $storeManager; + } + + /** + * @inheritdoc + */ + public function save( + \GhoSter\KbankPayments\Api\Data\TokenInterface $token + ) { + try { + $this->resource->save($token); + } catch (\Exception $exception) { + throw new CouldNotSaveException(__( + 'Could not save the token: %1', + $exception->getMessage() + )); + } + return $token; + } + + /** + * @inheritdoc + */ + public function getById($tokenId) + { + $token = $this->tokenFactory->create(); + $this->resource->load($token, $tokenId); + if (!$token->getId()) { + throw new NoSuchEntityException(__('Token with id "%1" does not exist.', $tokenId)); + } + return $token; + } + + /** + * @inheritdoc + */ + public function getByOrderIncrement($orderIncrementId) + { + $token = $this->tokenFactory->create(); + $this->resource->loadByOrderIncrement($token, $orderIncrementId); + if (!$token->getId()) { + throw new NoSuchEntityException( + __('Token with order increment id "%1" does not exist.', $orderIncrementId) + ); + } + return $token; + } + + /** + * @inheritdoc + */ + public function getByToken($token) + { + $tokenModel = $this->tokenFactory->create(); + $this->resource->loadByToken($tokenModel, $token); + if (!$tokenModel->getId()) { + throw new NoSuchEntityException( + __('Token with token value id "%1" does not exist.', $token) + ); + } + return $tokenModel; + } + + /** + * @inheritdoc + */ + public function getList( + SearchCriteriaInterface $searchCriteria + ) { + $collection = $this->tokenCollectionFactory->create(); + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + $fields = []; + $conditions = []; + foreach ($filterGroup->getFilters() as $filter) { + if ($filter->getField() === 'store_id') { + $collection->addStoreFilter($filter->getValue(), false); + continue; + } + $fields[] = $filter->getField(); + $condition = $filter->getConditionType() ?: 'eq'; + $conditions[] = [$condition => $filter->getValue()]; + } + $collection->addFieldToFilter($fields, $conditions); + } + + $sortOrders = $searchCriteria->getSortOrders(); + if ($sortOrders) { + /** @var SortOrder $sortOrder */ + foreach ($sortOrders as $sortOrder) { + $collection->addOrder( + $sortOrder->getField(), + ($sortOrder->getDirection() == SortOrder::SORT_ASC) ? 'ASC' : 'DESC' + ); + } + } + $collection->setCurPage($searchCriteria->getCurrentPage()); + $collection->setPageSize($searchCriteria->getPageSize()); + + $searchResults = $this->searchResultsFactory->create(); + $searchResults->setSearchCriteria($searchCriteria); + $searchResults->setTotalCount($collection->getSize()); + $searchResults->setItems($collection->getItems()); + return $searchResults; + } + + /** + * @inheritdoc + */ + public function delete( + \GhoSter\KbankPayments\Api\Data\TokenInterface $token + ) { + try { + $this->resource->delete($token); + } catch (\Exception $exception) { + throw new CouldNotDeleteException(__( + 'Could not delete the Token: %1', + $exception->getMessage() + )); + } + return true; + } + + /** + * @inheritdoc + */ + public function deleteById($tokenId) + { + return $this->delete($this->getById($tokenId)); + } +} diff --git a/Model/TransactionProcessor.php b/Model/TransactionProcessor.php new file mode 100644 index 0000000..3c79483 --- /dev/null +++ b/Model/TransactionProcessor.php @@ -0,0 +1,472 @@ +objectFactory = $objectFactory; + $this->dataObjectHelper = $dataObjectHelper; + $this->kbankClient = $kbankClient; + $this->kbankConfig = $kbankConfig; + $this->orderRepository = $orderRepository; + $this->transactionResponseValidator = $transactionResponseValidator; + $this->metaFactory = $metaFactory; + $this->invoiceSender = $invoiceSender; + $this->invoiceService = $invoiceService; + $this->transaction = $transaction; + $this->logger = $logger; + $this->tokenRepository = $tokenRepository; + $this->orderFactory = $orderFactory; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function authorizeOrderByToken( + Order $order, + TokenInterface $token + ): MetaInterface { + /** @var Meta $meta */ + $meta = $this->metaFactory->create(); + + $request = [ + 'method_type' => 'POST', + 'payload_type' => 'createAuthorizeRequest' + ]; + $data = [ + 'amount' => $this->_formatAmount($order->getGrandTotal()), + 'currency' => $order->getOrderCurrencyCode(), + 'description' => $order->getIncrementId(), + 'source_type' => 'card', + 'mode' => 'token', + 'reference_order' => $order->getIncrementId(), + 'token' => $token->getToken() + ]; + + $terminalId = $token->getTerminalId(); + if (!$terminalId) { + $terminalId = $order->getPayment()->getAdditionalInformation('terminal_id'); + } + + $smartpayId = $token->getSmartpayId(); + if (!$smartpayId) { + $smartpayId = $order->getPayment()->getAdditionalInformation('smartpay_id'); + } + + $term = $token->getTerm(); + if (!$term) { + $term = $order->getPayment()->getAdditionalInformation('term'); + } + + if (!empty($smartpayId) + && !empty($terminalId) + ) { + $data = array_merge($data, [ + 'additional_data' => [ + 'mid' => $token->getMid(), + 'tid' => $terminalId, + 'smartpay_id' => $smartpayId, + 'term' => $term + ] + ]); + } + + $dataPost = array_merge($request, $data); + $resultResponse = $this->kbankClient->postRequest($dataPost); + + $result = $this->transactionResponseValidator->validate( + array_merge($dataPost, ['response' => $resultResponse]) + ); + + if ($result->isValid()) { + $meta->setOrderId($order->getIncrementId()) + ->setCustomerId($order->getCustomerId()) + ->setChargeId($resultResponse['id']) + ->setObject($resultResponse['object']) + ->setAmount($resultResponse['amount']) + ->setCurrency($resultResponse['currency']) + ->setTransactionState($resultResponse['transaction_state']) + ->setCreated($resultResponse['created']) + ->setStatus($resultResponse['status']) + ->setApprovalCode($resultResponse['approval_code']) + ->setLivemode($resultResponse['livemode']) + ->setFailureCode((string)$resultResponse['failure_code']) + ->setFailureMessage((string)$resultResponse['failure_message']) + ->setRedirectUrl($resultResponse['redirect_url']) + ->setCardId($resultResponse['source']['id']) + ->setCardBrand($resultResponse['source']['brand']) + ->setIssuerBank($resultResponse['source']['issuer_bank']) + ->setMaskedPan($resultResponse['source']['card_masking']) + ->setRef1($resultResponse['ref_1']) + ->setRef2($resultResponse['ref_2']) + ->setRef3($resultResponse['ref_3']); + + $meta->save(); + + if ($meta->getTransactionState() == KbankPaymentsConfig::TRANSACTION_STATE_AUTH) { + $order->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $order->addStatusToHistory($order->getStatus(), __('Non 3DS Payment Authorized.')); + + if ($this->kbankConfig->isAutoInvoice() + && $order->canInvoice() + ) { + $this->createInvoice($order); + } + } elseif ($meta->getTransactionState() == KbankPaymentsConfig::TRANSACTION_STATE_PRE_AUTH) { + $order->addStatusToHistory($order->getStatus(), __('3DS Payment Pre-Authorized.')); + } + } else { + $message = 'Order is suspended as its authorizing amount of %1 is suspected to be fraudulent.'; + $order->addStatusToHistory( + $order->getStatus(), + __($message, $order->getIncrementId()) + ); + $order->registerCancellation(implode(', ', $result->getErrorCodes())); + } + + $this->orderRepository->save($order); + + return $meta; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function authorizeOrder(Order $order): MetaInterface + { + /** @var Meta $meta */ + $meta = $this->metaFactory->create(); + + try { + $token = $this->tokenRepository->getByOrderIncrement($order->getIncrementId()); + return $this->authorizeOrderByToken($order, $token); + } catch (\Exception $e) { + return $meta; + } + } + + /** + * @inheritDoc + */ + public function authorizeOrderByTokenValue(string $tokenValue): MetaInterface + { + /** @var Meta $meta */ + $meta = $this->metaFactory->create(); + + try { + $token = $this->tokenRepository->getByToken($tokenValue); + $orderIncrement = $token->getOrderId(); + $order = $this->orderFactory->create()->loadByIncrementId($orderIncrement); + + if ($order->getId()) { + return $this->authorizeOrderByToken($order, $token); + } else { + return $meta; + } + + } catch (\Exception $e) { + return $meta; + } + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function settleOrder(Order $order, $chargeId): array + { + $request = [ + 'method_type' => 'POST', + 'payload_type' => 'createSettleRequest' + ]; + $data = [ + 'charge_id' => $chargeId, + 'amount' => $this->_formatAmount($order->getGrandTotal()), + 'type' => 'full' + ]; + + $dataPost = array_merge($request, $data); + $resultResponse = $this->kbankClient->postRequest($dataPost); + + $result = $this->transactionResponseValidator->validate( + array_merge($dataPost, ['response' => $resultResponse]) + ); + + if ($result->isValid()) { + $order->addStatusToHistory($order->getStatus(), __('Payment Captured.')); + $this->orderRepository->save($order); + } + + return $resultResponse; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function transactionDetail(Order $order, $chargeId): array + { + $request = [ + 'method_type' => 'GET', + 'payload_type' => 'transactionDetailsRequest' + ]; + $data = [ + 'charge_id' => $chargeId, + ]; + + $dataPost = array_merge($request, $data); + + return $this->kbankClient->postRequest($dataPost); + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function voidOrder(Order $order, $chargeId): array + { + $request = [ + 'method_type' => 'POST', + 'payload_type' => 'voidRequest' + ]; + $data = [ + 'charge_id' => $chargeId, + 'reason' => 'Cancel transaction for reference_order: ' . $order->getIncrementId() + ]; + + $dataPost = array_merge($request, $data); + $resultResponse = $this->kbankClient->postRequest($dataPost); + + $result = $this->transactionResponseValidator->validate( + array_merge($dataPost, ['response' => $resultResponse]) + ); + + if ($result->isValid()) { + $message = 'Order is voided as its authorizing amount of %1 on gateway.'; + $order->addStatusToHistory( + $order->getStatus(), + __($message, $order->getIncrementId()) + ); + $order->registerCancellation($message); + } else { + $message = 'Cannot void order %1 on gateway.'; + $order->addStatusToHistory( + $order->getStatus(), + __($message, $order->getOrderIncrementId()) + ); + } + + $this->orderRepository->save($order); + + return $resultResponse; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function refundOrder(Order $order, $chargeId): array + { + $request = [ + 'method_type' => 'POST', + 'payload_type' => 'refundRequest' + ]; + + $data = [ + 'charge_id' => $chargeId, + 'amount' => $this->_formatAmount($order->getGrandTotal()), + 'reason' => 'Refund transaction for reference_order: ' . $order->getIncrementId() + ]; + + $dataPost = array_merge($request, $data); + $resultResponse = $this->kbankClient->postRequest($dataPost); + + $result = $this->transactionResponseValidator->validate( + array_merge($dataPost, ['response' => $resultResponse]) + ); + + if ($result->isValid()) { + $message = 'Order is refunded as its authorizing amount of %1 on gateway.'; + $order->addStatusToHistory( + $order->getStatus(), + __($message, $order->getIncrementId()) + ); + $order->setState(Order::STATE_COMPLETE) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_COMPLETE)); + } else { + $message = 'Cannot refund order %1 on gateway.'; + $order->addStatusToHistory( + $order->getStatus(), + __($message, $order->getOrderIncrementId()) + ); + } + + $this->orderRepository->save($order); + + return $resultResponse; + } + + /** + * @inheritDoc + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function createInvoice(Order $order): int + { + try { + $invoice = $this->invoiceService + ->prepareInvoice($order); + $invoice->register(); + $invoice->getOrder()->setIsInProcess(true); + $invoice->save(); + + $transactionSave = $this->transaction->addObject( + $invoice + )->addObject( + $invoice->getOrder() + ); + + $transactionSave->save(); + + $this->invoiceSender->send($invoice); + + $order->addStatusToHistory( + $order->getStatus(), + __('Notified customer about invoice creation #%1.', $invoice->getId()), + true + ); + + $result = $order->getId(); + } catch (\Exception $e) { + $this->logger->critical($e->getMessage()); + $result = $order->getId(); + } + + return $result; + } + + /** + * Format Amount + * + * @param float $orderAmount + * @return string + */ + protected function _formatAmount($orderAmount): string + { + return number_format((float)$orderAmount, 2, '.', ''); + } +} diff --git a/Model/Ui/ConfigProvider.php b/Model/Ui/ConfigProvider.php new file mode 100644 index 0000000..01a5d09 --- /dev/null +++ b/Model/Ui/ConfigProvider.php @@ -0,0 +1,134 @@ +config = $config; + $this->cart = $cart; + $this->urlBuilder = $urlBuilder; + $this->serializer = $serializer; + $this->installmentManagement = $installmentManagement; + + foreach ($methodCodes as $code) { + $this->methods[$code] = $paymentHelper->getMethodInstance($code); + } + } + + /** + * Retrieve assoc array of checkout configuration + * + * @return array + */ + public function getConfig(): array + { + $configValues = []; + $storeId = $this->cart->getStoreId(); + + foreach ($this->methods as $methodCode => $method) { + if ($method->isAvailable()) { + $configValues = array_merge_recursive($configValues, [ + 'payment' => [ + $methodCode => [ + 'processTokenActionUrl' => $this->getProcessTokenActionUrl(), + 'publicKey' => $this->config->getPublicKey($storeId), + 'jsSrc' => $this->config->getJsSrc($storeId), + 'environment' => $this->config->getEnvironment($storeId), + 'merchantId' => $method->getConfigData(Config::KEY_MERCHANT_ID), + 'terminalId' => $method->getConfigData(Config::KEY_TERMINAL_ID), + 'shopName' => $this->config->getShopName($storeId), + 'failureUrl' => $this->getFailureUrl(), + 'isUseInstallment' => (bool)$method->getConfigData(Config::KEY_INSTALLMENT_SUPPORT), + 'isPreDefinedInstallment' => (bool)$method->getConfigData( + Config::KEY_PRE_DEFINED_INSTALLMENT_SELECTION + ), + 'allowedInstallment' => + $this->installmentManagement->getAvailableInstallment($methodCode) + ], + ] + ]); + } + } + + return $configValues; + } + + /** + * Get popup action URL + * + * @return string + */ + protected function getProcessTokenActionUrl(): string + { + return $this->urlBuilder->getUrl('kbank/token/process', ['_secure' => true]); + } + + /** + * Get failure url + * + * @return string + */ + public function getFailureUrl(): string + { + return $this->urlBuilder->getUrl('checkout/onepage/failure', ['_secure' => true]); + } +} diff --git a/Model/Url.php b/Model/Url.php new file mode 100644 index 0000000..ab79657 --- /dev/null +++ b/Model/Url.php @@ -0,0 +1,82 @@ +urlBuilder = $urlBuilder; + } + + /** + * Get Success Url + * + * @return string + */ + public function getSuccessUrl(): string + { + return $this->getUrl('checkout/onepage/success'); + } + + /** + * Get Failure Url + * + * @return string + */ + public function getFailureUrl(): string + { + return $this->getUrl('checkout/onepage/failure'); + } + + /** + * Get Url + * + * @param string $route + * @param array $params + * @return string + */ + public function getUrl(string $route = '', array $params = []): string + { + return $this->urlBuilder->getUrl($route, $params); + } + + /** + * Set current order + * + * @param Order $order + * @return $this + */ + public function setOrder(Order $order): Url + { + $this->order = $order; + return $this; + } + + /** + * Get current order + * + * @return ?Order + */ + public function getOrder(): ?Order + { + return $this->order; + } +} diff --git a/Observer/InstallmentDataAssignObserver.php b/Observer/InstallmentDataAssignObserver.php new file mode 100644 index 0000000..f1f81b4 --- /dev/null +++ b/Observer/InstallmentDataAssignObserver.php @@ -0,0 +1,27 @@ +readDataArgument($observer); + + $additionalData = $data->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + if (!is_array($additionalData)) { + return; + } + + $paymentInfo = $this->readPaymentModelArgument($observer); + + foreach ($this->additionalInformationList as $additionalInformationKey) { + if (isset($additionalData[$additionalInformationKey])) { + $paymentInfo->setAdditionalInformation( + $additionalInformationKey, + $additionalData[$additionalInformationKey] + ); + } + } + } +} diff --git a/Observer/ProcessTokenDataObserver.php b/Observer/ProcessTokenDataObserver.php new file mode 100644 index 0000000..e57ba50 --- /dev/null +++ b/Observer/ProcessTokenDataObserver.php @@ -0,0 +1,137 @@ + 'smartpay_id', + 'smartpayid' => 'smartpay_id', + 'smartpay_id' => 'smartpay_id', + 'mid' => 'mid', + 'mId' => 'mid', + 'term' => 'term', + 'terminal_id' => 'terminal_id', + 'terminalId' => 'terminal_id', + 'token' => 'token', + 'paymentMethods' => 'payment_methods', + 'payment_methods' => 'payment_methods', + 'dcc_currency' => 'dcc_currency', + 'saveCard' => 'save_card', + 'save_card' => 'save_card', + 'cardid' => 'card_id', + 'card_id' => 'card_id' + ]; + + /** + * @var TransactionProcessor + */ + protected $transactionProcessor; + + /** + * @var TokenFactory + */ + protected $tokenFactory; + + /** + * @var TokenRepository + */ + protected $tokenRepository; + + /** + * @var PaymentHelper + */ + private $paymentHelper; + + /** + * DataAssignObserver constructor. + * + * @param TransactionProcessor $transactionProcessor + * @param TokenFactory $tokenFactory + * @param TokenRepository $tokenRepository + * @param PaymentHelper $paymentHelper + */ + public function __construct( + TransactionProcessor $transactionProcessor, + TokenFactory $tokenFactory, + TokenRepository $tokenRepository, + PaymentHelper $paymentHelper + ) { + $this->transactionProcessor = $transactionProcessor; + $this->tokenFactory = $tokenFactory; + $this->tokenRepository = $tokenRepository; + $this->paymentHelper = $paymentHelper; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + /** @var array $gatewayTokenData */ + $gatewayTokenData = $observer->getData('gateway_token'); + + /** @var Order $order */ + $order = $observer->getData('order'); + + /** @var DataObject $result */ + $result = $observer->getData('result'); + + if (empty($gatewayTokenData)) { + return $this; + } + + /** @var Token $token */ + $token = $this->tokenFactory->create(); + $token->setOrderId($order->getIncrementId()) + ->setCustomerId($order->getCustomerId()); + foreach ($this->additionalInformationMappingList as $dataKey => $dataMap) { + if (isset($gatewayTokenData[$dataKey])) { + $token->setData($dataMap, $gatewayTokenData[$dataKey]); + } + } + + $paymentCode = $order->getPayment()->getMethod(); + $paymentInstance = $this->paymentHelper->getMethodInstance($paymentCode); + + if (!$token->getTerminalId()) { + $token->setTerminalId( + $paymentInstance->getConfigData( + Config::KEY_TERMINAL_ID, + $order->getStoreId() + ) + ); + } + + try { + $token = $this->tokenRepository->save($token); + + $meta = $this->transactionProcessor + ->authorizeOrderByToken($order, $token); + + $observer->setData('meta', $meta); + } catch (\Exception $e) { + $result->setData('result', false) + ->setData('message', __('Sorry, but we can\'t authorize your order. Please try again later')); + } + return $this; + } +} diff --git a/Observer/ThreeDSValidationObserver.php b/Observer/ThreeDSValidationObserver.php new file mode 100644 index 0000000..4d6718c --- /dev/null +++ b/Observer/ThreeDSValidationObserver.php @@ -0,0 +1,120 @@ +transactionProcessor = $transactionProcessor; + $this->tokenFactory = $tokenFactory; + $this->kbankEmbeddedConfig = $kbankEmbeddedConfig; + $this->orderRepository = $orderRepository; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function execute(Observer $observer) + { + /** @var MetaInterface $meta */ + $meta = $observer->getData('meta'); + + /** @var Order $order */ + $order = $observer->getData('order'); + + /** @var DataObject */ + $errorObj = $observer->getData('error'); + $error = $errorObj->getError() ?? []; + $type = (string)$observer->getData('type'); + + try { + $resultResponse = $this->transactionProcessor + ->transactionDetail($order, $meta->getChargeId()); + + if (!isset($resultResponse['object']) + || !isset($resultResponse['transaction_state']) + ) { + $error[] = __('Invalid returned while inquiring or invalid transaction state'); + return $this; + } + + if ($resultResponse['object'] == KbankEmbeddedConfig::VALIDATION_3DS_VALIDATION_OBJECT + && $resultResponse['transaction_state'] == KbankEmbeddedConfig::TRANSACTION_STATE_AUTH + ) { + $order->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)); + $order->addStatusToHistory($order->getStatus(), __('3DS Payment Authorized.')); + + $this->logger->info(sprintf('3DS Payment Authorized by %s', $type)); + + if ($this->kbankEmbeddedConfig->isAutoInvoice() + && $order->canInvoice() + ) { + $this->transactionProcessor->createInvoice($order); + } + + $this->orderRepository->save($order); + } + } catch (\Exception $e) { + $error[] = $e->getMessage(); + } finally { + $errorObj->setData('error', $error); + } + + return $this; + } +} diff --git a/Plugin/Api/OrderRepositoryInterfacePlugin.php b/Plugin/Api/OrderRepositoryInterfacePlugin.php new file mode 100644 index 0000000..998dccb --- /dev/null +++ b/Plugin/Api/OrderRepositoryInterfacePlugin.php @@ -0,0 +1,104 @@ +metaRepository = $metaRepository; + $this->orderExtensionFactory = $orderExtensionFactory; + } + + /** + * Add more fields + * + * @param OrderRepositoryInterface $subject + * @param OrderInterface $order + * @return OrderInterface + */ + public function afterGet(OrderRepositoryInterface $subject, OrderInterface $order): OrderInterface + { + $this->addSalesExtensionData($order); + return $order; + } + + /** + * Add more fields + * + * @param OrderRepositoryInterface $subject + * @param OrderSearchResultInterface $orderSearchResult + * @return OrderSearchResultInterface + */ + public function afterGetList( + OrderRepositoryInterface $subject, + OrderSearchResultInterface $orderSearchResult + ): OrderSearchResultInterface { + foreach ($orderSearchResult->getItems() as $item) { + $this->addSalesExtensionData($item); + } + + return $orderSearchResult; + } + + /** + * Append sales order extension data + * + * @param OrderInterface $order + * @return void + */ + protected function addSalesExtensionData(OrderInterface $order) + { + $extensionAttributes = $order->getExtensionAttributes(); + $orderExtension = $extensionAttributes ?: $this->orderExtensionFactory->create(); + + $payment = $order->getPayment(); + + if (!in_array( + $payment->getMethod(), + [ + Config::METHOD_KBANK_EMBEDDED_INSTALLMENT, + Config::METHOD_KBANK_EMBEDDED_FULLPAYMENT, + Config::METHOD_KBANK_INLINE + ] + )) { + return; + } + + try { + $meta = $this->metaRepository->getByOrderIncrement($order->getIncrementId()); + if ($meta->getMetaId()) { + + $orderExtension->setCreditCardNo($meta->getMaskedPan()); + $orderExtension->setCreditType($meta->getCardBrand()); + + $order->setExtensionAttributes($orderExtension); + } + } catch (\Exception $e) { + return; + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e63e69 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Kasikornbank(Kbank) Magento 2 Payments Gateway Module # + + composer require ghoster/module-kbankpayments + +[![Latest Stable Version](https://poser.pugx.org/ghoster/module-kbankpayments/v)](https://packagist.org/packages/ghoster/module-kbankpayments) +[![Latest Unstable Version](https://poser.pugx.org/ghoster/module-kbankpayments/v/unstable)](https://packagist.org/packages/ghoster/module-kbankpayments) +[![License](https://poser.pugx.org/ghoster/module-outofstockatlast/license)](https://packagist.org/packages/ghoster/module-outofstockatlast) +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/thinghost) +[![Build Status](https://github.com/tuyennn/magento2-kbank-payment/actions/workflows/coding-standard.yml/badge.svg)](https://github.com/tuyennn/magento2-kbank-payment/actions/workflows/coding-standard.yml) +--- + +## Features + +* Module to add payment method Kbank (Kasikornbank) for Embedded UI Method +* Support 3DS +* Support Installment + +![kbank_embedded_installment](./.github/Screenshot/kbank_embedded_installment.png) +![kbank_embedded_full_payment](./.github/Screenshot/kbank_embedded_full_payment.png) + +## Compatibility + +| Magento Version (Open Source/Commerce) | Supported | +| -------------------------------------- | --------- | +| **2.0.x** | No ❌ | +| **2.1.x** | No ❌ | +| **2.2.x** | No ❌ | +| **<2.3.2** | No ❌ | +| **<2.3.5** | No ❌ | +| **>=2.3.5** | No ❌ | +| **2.4.0** | Yes ✔️ | +| **>=2.4.1 && < 2.4.6** | Yes ✔️ | +| **>=2.4.6** | Yes ✔️ | + +## Configurations +* Require the public and secret key from the Gateway: +* `/kbank/payment/callback` for Card Payment Callback URL: (Embedded Method only) +* `/rest/V1/kbank/payment/notify` for Card Payment Notify URL: (Embedded Method only) + +![Configuration](./.github/Screenshot/kbank_payment_configuration.png) + +## Donation + +If this project help you reduce time to develop, you can give me a cup of beer :) + +[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/thinghost) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f4b7023 --- /dev/null +++ b/composer.json @@ -0,0 +1,54 @@ +{ + "name": "ghoster/module-kbankpayments", + "description": "Kasikornbank Magento 2 Payments Gateway Module", + "keywords": [ + "magento 2", + "magento", + "m2", + "kasikornbank payment", + "kbank payment", + "magento 2 extension", + "magento 2 extension free" + ], + "require": { + "magento/framework": "^103.0" + }, + "minimum-stability": "stable", + "archive": { + "exclude": [ + "/.gitignore", + "/grumphp.yml", + "/pdepend.xml", + "/phpstan.neon", + "/phpunit.xml", + "/phpcs.xml", + "/phpmd.xml", + "/package.json", + "/.eslintrc.json", + "/.eslintignore", + "/tests", + "/.github" + ] + }, + "type": "magento2-module", + "version": "1.0.0", + "authors": [ + { + "name": "Tuyen Nguyen", + "email": "thinghost76@gmail.com", + "homepage": "https://thinghost.info/" + } + ], + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "GhoSter\\KbankPayments\\": "" + } + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..4109a6c --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,20 @@ + + + +
+ +
Accept online payments using Kbank Embedded Method. K-Payment Gateway provided by KASIKORNBANK Public Company Limited (hereinafter + referred to as “KBank”) is the online payment service to facilitate merchants and offers + customers more online payment options. + K-Payment Gateway merchant is able to integrate with APIs and also test on 24x7 + available sandbox environment provided by KBank before go live.
]]> +
+ complex kbank-admin-config kbank-section + GhoSter\KbankPayments\Block\Adminhtml\Config\Fieldset + + + +
+
+
+
diff --git a/etc/adminhtml/system/basic.xml b/etc/adminhtml/system/basic.xml new file mode 100644 index 0000000..6a926fa --- /dev/null +++ b/etc/adminhtml/system/basic.xml @@ -0,0 +1,56 @@ + + + + + <p>You will need to set your API keys before you can accept payments. If you do not yet have a Kbank account, please <a href="https://kasikornbank.com" target="_blank" rel="noopener noreferrer">click here</a> to register.</p> + + <p>For additional technical support contact <a href="mailto:info@kasikornbank.com" target="_blank" rel="noopener noreferrer">info@kasikornbank.com</a>. Or access <a href="https://apiportal.kasikornbank.com/product/content/All/K%20PAYMENT%20GATEWAY/Getting%20Started/K%20PAYMENT%20GATEWAY" target="_blank" rel="noopener noreferrer">API Portal</a>. </p> + + + + + Magento\Config\Model\Config\Backend\Encrypted + ghoster_kbank/general/public_key + + + + Magento\Config\Model\Config\Backend\Encrypted + ghoster_kbank/general/secret_key + + + + GhoSter\KbankPayments\Model\Adminhtml\Source\Environment + ghoster_kbank/general/environment + + + + ghoster_kbank/general/shop_name + + + + Magento\Config\Model\Config\Source\Yesno + ghoster_kbank/general/is_3ds_support + + + + Magento\Config\Model\Config\Source\Yesno + ghoster_kbank/general/debug + + + + Magento\Config\Model\Config\Source\Yesno + ghoster_kbank/general/auto_invoice + + + + validate-digits validate-digits-range digits-range-1- + ghoster_kbank/general/expired_pending_time + + + + Kbank Portal.]]> + GhoSter\KbankPayments\Block\Adminhtml\Config\Form\Field\Webhook + + + diff --git a/etc/adminhtml/system/embedded_fullpayment.xml b/etc/adminhtml/system/embedded_fullpayment.xml new file mode 100644 index 0000000..0a913a7 --- /dev/null +++ b/etc/adminhtml/system/embedded_fullpayment.xml @@ -0,0 +1,55 @@ + + + + + <p>The settings on these fields should be related only with Embedded Settings</p> + + + + Magento\Config\Model\Config\Source\Yesno + payment/kbank_embedded_fullpayment/active + + + + payment/kbank_embedded_fullpayment/title + + + + GhoSter\KbankPayments\Model\Adminhtml\Source\PaymentAction + payment/kbank_embedded_fullpayment/payment_action + + + + payment/kbank_embedded_fullpayment/merchant_id + + + + payment/kbank_embedded_fullpayment/terminal_id + + + + Magento\Sales\Model\Config\Source\Order\Status\Newprocessing + payment/kbank_embedded_fullpayment/order_status + + + + Magento\Payment\Model\Config\Source\Allspecificcountries + payment/kbank_embedded_fullpayment/allowspecific + + + + Magento\Directory\Model\Config\Source\Country + payment/kbank_embedded_fullpayment/specificcountry + + + + validate-number + payment/kbank_embedded_fullpayment/sort_order + + + + payment/kbank_embedded_fullpayment/instructions + + + diff --git a/etc/adminhtml/system/embedded_installment.xml b/etc/adminhtml/system/embedded_installment.xml new file mode 100644 index 0000000..a6211c7 --- /dev/null +++ b/etc/adminhtml/system/embedded_installment.xml @@ -0,0 +1,82 @@ + + + + + <p>The settings on these fields should be related only with Embedded Settings</p> + + + + Magento\Config\Model\Config\Source\Yesno + payment/kbank_embedded_installment/active + + + + payment/kbank_embedded_installment/title + + + + GhoSter\KbankPayments\Model\Adminhtml\Source\PaymentAction + payment/kbank_embedded_installment/payment_action + + + + payment/kbank_embedded_installment/merchant_id + + + + payment/kbank_embedded_installment/terminal_id + + + + Magento\Config\Model\Config\Source\Yesno + payment/kbank_embedded_installment/predefine_installment + + 1 + + + + + payment/kbank_embedded_installment/smartpay_id + + 1 + 0 + + + + + GhoSter\KbankPayments\Block\Adminhtml\System\Config\SmartPayId + GhoSter\KbankPayments\Model\Config\Backend\SmartPayId + + + 1 + 1 + + payment/kbank_embedded_installment/installment_info + + + + Magento\Sales\Model\Config\Source\Order\Status\Newprocessing + payment/kbank_embedded_installment/order_status + + + + Magento\Payment\Model\Config\Source\Allspecificcountries + payment/kbank_embedded_installment/allowspecific + + + + Magento\Directory\Model\Config\Source\Country + payment/kbank_embedded_installment/specificcountry + + + + validate-number + payment/kbank_embedded_installment/sort_order + + + + payment/kbank_embedded_installment/instructions + + + diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..8869413 --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,122 @@ + + + + + + sandbox + Your Shop Name + 1 + 1 + 1 + 10 + + + + + + + 0 + GhoSterKbankEmbeddedInstallmentFacade + Kbank Embedded Installment + pending + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 1 + 0 + 0 + 1 + 1 + 0 + 1 + 1 + 0001 + 0 + 1 + 0 + + + 0001 + 3 + 3 - 0.00% + 1 + + + 0001 + 4 + 4 - 0.00% + 1 + + + 0001 + 5 + 5 - 0.00% + 1 + + + 0001 + 6 + 6 - 0.00% + 1 + + + 0002 + 3 + 3 - 0.65% + 1 + + + 0002 + 4 + 4 - 0.65% + 1 + + + 0002 + 5 + 5 - 0.65% + 1 + + + 0002 + 6 + 6 - 0.65% + 1 + + + 10 + + + 0 + GhoSterKbankEmbeddedFullPaymentFacade + Kbank Embedded Full Payment + pending + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 1 + 0 + 0 + 1 + 1 + 0 + 1 + 1 + 0 + 0 + 10 + + + + + diff --git a/etc/crontab.xml b/etc/crontab.xml new file mode 100644 index 0000000..3ee8eb3 --- /dev/null +++ b/etc/crontab.xml @@ -0,0 +1,9 @@ + + + + + */5 * * * * + + + + diff --git a/etc/csp_whitelist.xml b/etc/csp_whitelist.xml new file mode 100644 index 0000000..9c7e925 --- /dev/null +++ b/etc/csp_whitelist.xml @@ -0,0 +1,30 @@ + + + + + + *.kasikornbank.com + + + + + *.kasikornbank.com + + + + + *.kasikornbank.com + + + + + *.kasikornbank.com + + + + + *.kasikornbank.com + + + + diff --git a/etc/db_schema.xml b/etc/db_schema.xml new file mode 100644 index 0000000..6341763 --- /dev/null +++ b/etc/db_schema.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json new file mode 100644 index 0000000..ba0bbf7 --- /dev/null +++ b/etc/db_schema_whitelist.json @@ -0,0 +1,67 @@ +{ + "ghoster_kbank_token": { + "column": { + "token_id": true, + "customer_id": true, + "token": true, + "order_id": true, + "paymentMethods": true, + "created_time": true, + "mid": true, + "dcc_currency": true, + "smartpayid": true, + "term": true, + "saveCard": true, + "cardid": true, + "campaign_id": true, + "terminal_id": true + }, + "index": { + "GHOSTER_KBANK_TOKEN_TOKEN_ID": true + }, + "constraint": { + "PRIMARY": true, + "GHOSTER_KBANK_TOKEN_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID": true + } + }, + "ghoster_kbank_meta": { + "column": { + "meta_id": true, + "order_id": true, + "customer_id": true, + "charge_id": true, + "object": true, + "amount": true, + "currency": true, + "transaction_state": true, + "created": true, + "status": true, + "approval_code": true, + "livemode": true, + "metadata": true, + "failure_code": true, + "failure_message": true, + "redirect_url": true, + "card_id": true, + "card_brand": true, + "issuer_bank": true, + "masked_pan": true, + "ref_1": true, + "ref_2": true, + "ref_3": true, + "baht_amount": true, + "rate": true, + "converted_amount": true, + "converted_currency_code": true, + "converted_currency_name": true, + "f63": true, + "mid": true + }, + "index": { + "GHOSTER_KBANK_META_META_ID": true + }, + "constraint": { + "PRIMARY": true + } + } +} diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 0000000..2bdd5c6 --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_INSTALLMENT + + + + + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_FULLPAYMENT + + + + + + + /kbank/token/ + /kbank/payment/ + + + + + + + GhoSterKbankEmbeddedInstallmentConfig + + + + + GhoSterKbankEmbeddedFullPaymentConfig + + + + + + + GhoSterKbankEmbeddedInstallmentValueHandler + + + + + + + GhoSterKbankEmbeddedFullPaymentValueHandler + + + + + + GhoSterKbankEmbeddedInstallmentConfig + + + + + GhoSterKbankEmbeddedFullPaymentConfig + + + + + + GhoSterKbankEmbeddedInstallmentCountryValidator + + + + + + + GhoSterKbankEmbeddedFullPaymentCountryValidator + + + + + + GhoSterKbankEmbeddedInstallmentConfig + + + + + GhoSterKbankEmbeddedFullPaymentConfig + + + + + + + + GhoSterKbankVoidCommand + GhoSterKbankEmbeddedRefundCommand + GhoSterKbankEmbeddedRefundSettledCommand + GhoSterKbankCancelCommand + GhoSterKbankTransactionDetailsCommand + GhoSterKbankEmbeddedFetchTransactionInfoCommand + GhoSterKbankEmbeddedDoNothingCommand + + + + + + + + + GhoSterKbankVoidRequest + GhoSterKbankDefaultTransferFactory + GhoSter\KbankPayments\Gateway\Http\Client + GhoSter\KbankPayments\Logger\Logger + GhoSterKbankVoidHandler + GhoSterKbankTransactionValidator + + + + + GhoSterKbankEmbeddedCommandPool + + + + + GhoSterKbankEmbeddedRefundRequest + GhoSterKbankDefaultTransferFactory + GhoSter\KbankPayments\Gateway\Http\Client + GhoSter\KbankPayments\Logger\Logger + GhoSterKbankTransactionValidator + GhoSterKbankEmbeddedPaymentRefundHandler + + + + + GhoSterKbankEmbeddedCommandPool + + + + + GhoSterKbankTransactionDetailsRequest + GhoSterKbankDefaultTransferFactory + GhoSter\KbankPayments\Gateway\Http\Client + GhoSterKbankTransactionDetailsValidator + + + + + GhoSterKbankCancelHandler + + + + + GhoSterKbankEmbeddedCommandPool + + + + + + + + + GhoSterKbankTransactionDetailsRequestTypeBuilder + GhoSterKbankRequestMethodGetBuilder + GhoSter\KbankPayments\Gateway\Request\TransactionDetailsDataBuilder + + + + + + + GhoSterKbankVoidRequestTypeBuilder + GhoSterKbankRequestMethodPostBuilder + GhoSter\KbankPayments\Gateway\Request\VoidDataBuilder + + + + + + + GhoSterKbankEmbeddedRefundRequestTypeBuilder + GhoSterKbankRequestMethodPostBuilder + GhoSter\KbankPayments\Gateway\Request\RefundDataBuilder + + + + + + + + + GhoSter\KbankPayments\Gateway\Response\Handler\VoidResponseHandler + + + + + + false + + + + + + GhoSter\KbankPayments\Gateway\Response\Handler\ChargeIdHandler + GhoSter\KbankPayments\Gateway\Response\Handler\CloseParentTransactionHandler + GhoSter\KbankPayments\Gateway\Response\Handler\CloseTransactionHandler + + + + + + + GhoSter\KbankPayments\Gateway\Response\Handler\CloseTransactionHandler + GhoSter\KbankPayments\Gateway\Response\Handler\CloseParentTransactionHandler + GhoSter\KbankPayments\Gateway\Response\Handler\Cancel\CancelOrder + + + + + + + + + + store_id + + + + + + + GhoSterKbankRemoveStoreConfigFilter + + + + + + + + + transactionDetailsRequest + + + + + voidRequest + + + + + refundRequest + + + + + POST + + + + + GET + + + + + + + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_INSTALLMENT + GhoSter\KbankPayments\Block\Form + GhoSterKbankEmbeddedInstallmentInfoBlock + GhoSterKbankEmbeddedInstallmentValueHandlerPool + GhoSterEmbeddedInstallmentValidatorPool + GhoSterKbankEmbeddedCommandPool + + + + + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_FULLPAYMENT + GhoSter\KbankPayments\Block\Form + GhoSterKbankEmbeddedFullPaymentInfoBlock + GhoSterKbankEmbeddedFullPaymentValueHandlerPool + GhoSterEmbeddedFullPaymentValidatorPool + GhoSterKbankEmbeddedCommandPool + + + + + + + + true + + + GhoSter\KbankPayments\Gateway\Validator\GeneralResponseValidator + GhoSter\KbankPayments\Gateway\Validator\TransactionResponseValidator + + + + + + + GhoSter\KbankPayments\Gateway\Validator\GeneralResponseValidator + + + + + + + + + ghoster_kbank_logger + + GhoSter\KbankPayments\Logger\PaymentHandler + + + + + + Magento\Framework\Filesystem\Driver\File + /var/log/kbank_payments.log + + + + + + + GhoSter\KbankPayments\Logger\Logger + + + + + GhoSter\KbankPayments\Logger\Logger + + + + + GhoSter\KbankPayments\Logger\Logger + + + + + GhoSter\KbankPayments\Logger\Logger + + + + + GhoSter\KbankPayments\Logger\Logger + + + + + GhoSter\KbankPayments\Logger\Logger + + + diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..fa15d81 --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/etc/extension_attributes.xml b/etc/extension_attributes.xml new file mode 100644 index 0000000..a4ac022 --- /dev/null +++ b/etc/extension_attributes.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml new file mode 100644 index 0000000..cac9e44 --- /dev/null +++ b/etc/frontend/di.xml @@ -0,0 +1,36 @@ + + + + + + 1 + + + + + + + GhoSterKbankEmbeddedInstallmentConfig + + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_INSTALLMENT + GhoSter\KbankPayments\Gateway\Config::METHOD_KBANK_EMBEDDED_FULLPAYMENT + + + + + + + + GhoSterKbankEmbeddedConfigProvider + + + + + + + + /kbank/ + + + + diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml new file mode 100644 index 0000000..3cbb50b --- /dev/null +++ b/etc/frontend/routes.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/etc/module.xml b/etc/module.xml new file mode 100644 index 0000000..ae44c3b --- /dev/null +++ b/etc/module.xml @@ -0,0 +1,5 @@ + + + + diff --git a/etc/payment.xml b/etc/payment.xml new file mode 100644 index 0000000..bbe80d8 --- /dev/null +++ b/etc/payment.xml @@ -0,0 +1,11 @@ + + + + + 0 + + + 0 + + + diff --git a/etc/webapi.xml b/etc/webapi.xml new file mode 100644 index 0000000..aff2878 --- /dev/null +++ b/etc/webapi.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/etc/webapi_rest/di.xml b/etc/webapi_rest/di.xml new file mode 100644 index 0000000..7368ad5 --- /dev/null +++ b/etc/webapi_rest/di.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/registration.php b/registration.php new file mode 100644 index 0000000..28b0e4e --- /dev/null +++ b/registration.php @@ -0,0 +1,7 @@ + + + + + + diff --git a/view/adminhtml/web/images/logo-kbank.png b/view/adminhtml/web/images/logo-kbank.png new file mode 100644 index 0000000..8a6ea31 Binary files /dev/null and b/view/adminhtml/web/images/logo-kbank.png differ diff --git a/view/adminhtml/web/styles.css b/view/adminhtml/web/styles.css new file mode 100644 index 0000000..fc98a5b --- /dev/null +++ b/view/adminhtml/web/styles.css @@ -0,0 +1,4 @@ +.kbank-section .kbank-payment-logo {background: url("./images/logo-kbank.png") no-repeat 0 40% / 18rem auto; + height: 100px; width: auto; padding-left: 20rem; display:inline-block; vertical-align: middle;} +.kbank-section .button-container {float: right;} +.kbank-section .kbank-payment-text { display: inline-block; vertical-align: middle; width: 50%} diff --git a/view/base/web/js/view/payment/kbankjs-client.js b/view/base/web/js/view/payment/kbankjs-client.js new file mode 100644 index 0000000..cc36c3b --- /dev/null +++ b/view/base/web/js/view/payment/kbankjs-client.js @@ -0,0 +1,72 @@ +define([ + 'jquery', + 'uiClass', + 'GhoSter_KbankPayments/js/view/payment/validator-handler' + //phpcs:ignore Squiz.Functions.MultiLineFunctionDeclaration.SpaceBeforeOpenParen +], function ( + $, + Class, + validatorHandler +) { + 'use strict'; + + return Class.extend({ + defaults: { + environment: 'production' + }, + + /** + * @{inheritdoc} + */ + initialize: function () { + validatorHandler.initialize(); + + this._super(); + + return this; + }, + + /** + * Creates the token pair with the provided data + * + * @param {Object} data + * @return {jQuery.Deferred} + */ + createTokens: function (data) { + var deferred = $.Deferred(); + + this._createTokens(deferred, data); + + return deferred.promise(); + }, + + /** + * Creates a token from the payment information in the form + * + * @param {jQuery.Deferred} deferred + * @param {Object} data + */ + _createTokens: async function (deferred, data) { + if (window.hasOwnProperty('KInlineCheckout') && typeof window.KInlineCheckout !== 'undefined') { + + let token = await window.KInlineCheckout.getToken() + .then((result) => { + return result; + }); + + validatorHandler.validate(token, function (valid, messages) { + if (valid) { + deferred.resolve({ + tokenValue: token['token'], + cardData: token.hasOwnProperty('card') ? token['card'] : null + }); + } else { + deferred.reject(messages); + } + }); + } else { + deferred.reject(['The payment gateway was not ready.']); + } + } + }); +}); diff --git a/view/base/web/js/view/payment/response-validator.js b/view/base/web/js/view/payment/response-validator.js new file mode 100644 index 0000000..fabb97f --- /dev/null +++ b/view/base/web/js/view/payment/response-validator.js @@ -0,0 +1,29 @@ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return { + /** + * Validate Kbank response + * + * @param {Object} context + * @returns {jQuery.Deferred} + */ + validate: function (context) { + var state = $.Deferred(), + messages = []; + + if (context.hasOwnProperty('token')) { + state.resolve(); + } else { + messages.push($t('There is an error during getting token.')); + state.reject(messages); + } + + return state.promise(); + } + }; +}); + diff --git a/view/base/web/js/view/payment/validator-handler.js b/view/base/web/js/view/payment/validator-handler.js new file mode 100644 index 0000000..a4e1f47 --- /dev/null +++ b/view/base/web/js/view/payment/validator-handler.js @@ -0,0 +1,54 @@ +define([ + 'jquery', + 'GhoSter_KbankPayments/js/view/payment/response-validator' +], function ($, responseValidator) { + 'use strict'; + + return { + validators: [], + + /** + * Init list of validators + */ + initialize: function () { + this.add(responseValidator); + }, + + /** + * Add new validator + * @param {Object} validator + */ + add: function (validator) { + this.validators.push(validator); + }, + + /** + * Run pull of validators + * @param {Object} context + * @param {Function} callback + */ + validate: function (context, callback) { + var self = this, + deferred; + + // no available validators + if (!self.validators.length) { + callback(true); + + return; + } + + // get list of deferred validators + deferred = $.map(self.validators, function (current) { + return current.validate(context); + }); + + $.when.apply($, deferred) + .done(function () { + callback(true); + }).fail(function (error) { + callback(false, error); + }); + } + }; +}); diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml new file mode 100644 index 0000000..ec7cc7c --- /dev/null +++ b/view/frontend/layout/checkout_index_index.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + uiComponent + + + + + + + + GhoSter_KbankPayments/js/view/payment/kbank-embedded-payments + + + true + + + true + + + + + + + + + + + + + + + + + + + diff --git a/view/frontend/templates/hss/js.phtml b/view/frontend/templates/hss/js.phtml new file mode 100644 index 0000000..24a976a --- /dev/null +++ b/view/frontend/templates/hss/js.phtml @@ -0,0 +1,6 @@ + +
diff --git a/view/frontend/web/css/source/_module.less b/view/frontend/web/css/source/_module.less new file mode 100644 index 0000000..32d1eef --- /dev/null +++ b/view/frontend/web/css/source/_module.less @@ -0,0 +1,97 @@ + +// +// Common +// _____________________________________________ +@payment-active__color: #00ab86; + +& when (@media-common = true) { + .checkout-payment-method { + .payment-method-content { + &#kbank-payment-method { + padding-top: 15px; + + label { + font-size: 1em; + margin-bottom: 10px; + + &::after { + content: '*'; + color: #e02b27; + font-size: 1.2rem; + margin: 0 0 0 5px; + } + } + + .field { + margin-bottom: 10px; + } + + .input-mask { + border: 1px solid #ccc !important; + border-radius: 5px; + padding: 3px 10px 2px; + width: 225px; + } + + iframe { + width: 250px; + } + } + } + } + + .kbank-installment { + .payment-method-type { + display: flex; + } + + .bank-installment-item { + background-color: #F2F2F2; + border: 1px solid transparent; + border-radius: 5px; + margin: 5px; + position: relative; + padding: 10px; + text-align: center; + min-width: 64px; + min-height: 32px; + + label { + padding-left: 0 !important; + + &:before { + display: none !important; + } + } + + &.selected-item { + border-color: @payment-active__color; + } + + img { + max-width: 80px; + } + + label { + display: block; + margin-top: 5px; + text-align: center; + } + } + } + + .checkout-index-index { + .modal-popup.kbank-embedded-form-box { + .modal-content { + padding-bottom: 20px; + } + } + } + + .kbank-embedded-form-box { + .kbank.iframe { + height: 100%; + min-height: 600px; + } + } +} diff --git a/view/frontend/web/images/icons/icon-checked.svg b/view/frontend/web/images/icons/icon-checked.svg new file mode 100644 index 0000000..b2cdadc --- /dev/null +++ b/view/frontend/web/images/icons/icon-checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/view/frontend/web/js/action/validate-order-status.js b/view/frontend/web/js/action/validate-order-status.js new file mode 100644 index 0000000..f44167a --- /dev/null +++ b/view/frontend/web/js/action/validate-order-status.js @@ -0,0 +1,35 @@ +define( + [ + 'ko', + 'jquery', + 'Magento_Checkout/js/model/url-builder', + 'mage/storage' + ], + function ( + ko, + $, + urlBuilder, + storage + ) { + 'use strict'; + + return function (orderId, deferred) { + var serviceUrl; + + deferred = deferred || $.Deferred(); + + serviceUrl = urlBuilder.createUrl('/kbank/:orderId/validate-order-status', { + orderId: orderId + }); + + return storage.get( + serviceUrl, + false + ).done(function (response) { + deferred.resolve(); + }).fail(function (response) { + deferred.reject(); + }); + }; + } +); diff --git a/view/frontend/web/js/model/alert.js b/view/frontend/web/js/model/alert.js new file mode 100644 index 0000000..59d7e74 --- /dev/null +++ b/view/frontend/web/js/model/alert.js @@ -0,0 +1,54 @@ +define([ + 'jquery', + 'underscore', + 'mage/translate', + 'jquery/ui', + 'Magento_Ui/js/modal/confirm' +], function ($, _, $t) { + 'use strict'; + + $.widget('mage.alert', $.mage.confirm, { + options: { + modalClass: 'confirm', + title: $.mage.__('Attention'), + actions: { + + /** + * Callback always - called on all actions. + */ + always: function () {} + }, + buttons: [{ + text: $.mage.__('OK'), + class: 'action-primary action-accept', + + /** + * Click handler. + */ + click: function () { + this.closeModal(true); + } + }] + }, + + /** + * Close modal window. + */ + closeModal: function () { + this.options.actions.always(); + this.element.bind('alertclosed', _.bind(this._remove, this)); + var textOrder = $.mage.__('Place Order'), + placeOrderText = $('button.ghoster').find('span'); + + placeOrderText.attr('data-bind', 'i18n: \'' + textOrder + '\''); + placeOrderText.text($t(textOrder)); + + + return this._super(); + } + }); + + return function (config) { + return $('
').html(config.content).alert(config); + }; +}); diff --git a/view/frontend/web/js/model/installment.js b/view/frontend/web/js/model/installment.js new file mode 100644 index 0000000..79e884a --- /dev/null +++ b/view/frontend/web/js/model/installment.js @@ -0,0 +1,50 @@ +/** + * @api + */ +define([ + 'ko', + 'underscore', + 'domReady!' +], function (ko, _) { + 'use strict'; + + var proceedInstallmentData = function (data) { + let installmentData = {}; + if (_.isObject(data)) { + installmentData['payment_term'] = data.hasOwnProperty('payment_term') ? data.payment_term : null; + installmentData['smartpay_id'] = data.hasOwnProperty('smartpay_id') ? data.smartpay_id : null; + installmentData['installment_title'] = data.hasOwnProperty('installment_title') ? data.installment_title : null; + } + return installmentData; + }, + installmentData = proceedInstallmentData(window.checkoutConfig.quoteData), + installmentInformation = ko.observable(installmentData), + quoteData = window.checkoutConfig.quoteData; + + return { + installmentInformation: installmentInformation, + + /** + * @return {*} + */ + getQuoteId: function () { + return quoteData['entity_id']; + }, + + /** + * + * @return {*} + */ + getInstallmentInformation: function () { + return installmentInformation; + }, + + /** + * @param {Object} data + */ + setInstallmentInformation: function (data) { + data = proceedInstallmentData(data); + installmentInformation(data); + } + }; +}); diff --git a/view/frontend/web/js/view/payment/kbank-embedded-payments.js b/view/frontend/web/js/view/payment/kbank-embedded-payments.js new file mode 100644 index 0000000..aaa631d --- /dev/null +++ b/view/frontend/web/js/view/payment/kbank-embedded-payments.js @@ -0,0 +1,23 @@ +define( + [ + 'uiComponent', + 'Magento_Checkout/js/model/payment/renderer-list' + ], + function ( + Component, + rendererList + ) { + 'use strict'; + rendererList.push( + { + type: 'kbank_embedded_fullpayment', + component: 'GhoSter_KbankPayments/js/view/payment/method-renderer/kbank-embedded-fullpayment-method' + }, + { + type: 'kbank_embedded_installment', + component: 'GhoSter_KbankPayments/js/view/payment/method-renderer/kbank-embedded-installment-method' + } + ); + return Component.extend({}); + } +); diff --git a/view/frontend/web/js/view/payment/kbank-embedded/installment-renderer/default.js b/view/frontend/web/js/view/payment/kbank-embedded/installment-renderer/default.js new file mode 100644 index 0000000..940bb8c --- /dev/null +++ b/view/frontend/web/js/view/payment/kbank-embedded/installment-renderer/default.js @@ -0,0 +1,63 @@ +define([ + 'jquery', + 'ko', + 'uiComponent', + 'underscore', + 'GhoSter_KbankPayments/js/model/installment' +], function ($, ko, Component, _, installment) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'GhoSter_KbankPayments/payment/installment-renderer/default' + }, + + /** + * @returns {*} + */ + initObservable: function () { + this._super(); + this.isSelected = ko.computed(function () { + var isSelected = false, + installmentInformation = installment.installmentInformation(); + + if (installmentInformation) { + isSelected = installmentInformation.smartpay_id == this.installment().smartpay_id && + installmentInformation.payment_term == this.installment().payment_term; //eslint-disable-line eqeqeq + } + + return isSelected; + }, this); + + return this; + }, + + /** + * @inheritdoc + */ + isChecked: ko.computed(function () { + return installment.installmentInformation() ? installment.installmentInformation().smartpay_id : null; + }), + + /** + * @inheritdoc + */ + selectInstallment: function () { + installment.installmentInformation(this.installment()); + }, + + /** + * @returns {string} + */ + getInstallmentElmId: function () { + return this.paymentMethodCode() + '-smartpayid-' + this.installment().smartpay_id; + }, + + /** + * @returns {string} + */ + getInstallmentElmName: function () { + return 'payment[' + this.paymentMethodCode() + '][smartpayid-' + this.installment().smartpay_id + ']'; + } + }); +}); diff --git a/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-fullpayment-method.js b/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-fullpayment-method.js new file mode 100644 index 0000000..a1dd403 --- /dev/null +++ b/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-fullpayment-method.js @@ -0,0 +1,271 @@ +define( + [ + 'Magento_Checkout/js/view/payment/default', + 'ko', + 'jquery', + 'underscore', + 'Magento_Checkout/js/action/place-order', + 'Magento_Checkout/js/model/full-screen-loader', + 'Magento_Checkout/js/model/quote', + 'GhoSter_KbankPayments/js/action/validate-order-status', + 'Magento_Ui/js/modal/modal', + 'mage/translate' + ], + function ( + Component, + ko, + $, + _, + placeOrderAction, + fullScreenLoader, + quote, + validateOrderStatusAction + ) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'GhoSter_KbankPayments/payment/kbank-embedded-fullpayment', + paymentReady: false, + inActionBodyClass: 'kbank-embedded-in-action', + grandTotalAmount: null, + embeddedFormPopup: null, + orderId: null, + isUseInstallment: false, + rendererTemplates: [], + embeddedFormElmPrefix: 'kbank-embedded-', + embeddedFormElmPostfix: '-hidden' + }, + redirectAfterPlaceOrder: false, + isInitializedTrigger: ko.observable(false), + isPopupClicked: ko.observable(false), + isInAction: ko.observable(false), + + initialize: function () { + this._super(); + var context = this; + + $(document) + .on('ghoster:closeKbankPopup', function () { + context.isInAction(false); + }); + + this.isUseInstallment = false; + }, + + /** @inheritdoc */ + initConfig: function () { + this._super(); + // the list of child components that are responsible for address rendering + return this; + }, + + /** + * Initialize form elements + */ + initKbankFormElement: function () { + var script, + hiddenEmbeddedFormElmId = this.getHiddenButtonEmbeddedFormElmId(); + + script = document.querySelector('script[src^="' + this.getKbankSrc() + '"]'); + + if (!script) { + script = document.createElement('script'); + script.src = this.getKbankSrc(); + this.setDataForScript(script); + document.getElementById(hiddenEmbeddedFormElmId).appendChild(script); + } else { + this.setDataForScript(script); + } + }, + + /** + * Set data for script + * + * @param script + */ + setDataForScript: function (script) { + script.setAttribute('data-apikey', window.checkoutConfig.payment[this.getCode()].publicKey); + script.setAttribute('data-amount', quote.totals()['base_grand_total']); + script.setAttribute('data-currency', window.checkoutConfig.quoteData.quote_currency_code); + script.setAttribute('data-payment-methods', 'card'); + script.setAttribute('data-name', window.checkoutConfig.payment[this.getCode()]['shopName']); + script.setAttribute('data-mid', window.checkoutConfig.payment[this.getCode()]['merchantId']); + }, + + /** + * Get action url for payment method popup. + * @returns {String} + */ + getTokenProcessActionUrl: function () { + return window.checkoutConfig.payment[this.item.method]['processTokenActionUrl']; + }, + + /** + * @returns {String} + */ + getEmbeddedFormElmId: function () { + return this.embeddedFormElmPrefix + this.getCode(); + }, + + /** + * @returns {String} + */ + getHiddenButtonEmbeddedFormElmId: function () { + return this.embeddedFormElmPrefix + this.getCode() + this.embeddedFormElmPostfix; + }, + + /** + * @returns {String} + */ + getKbankSrc: function () { + return window.checkoutConfig.payment[this.getCode()].jsSrc; + }, + + /** + * @returns {String} + */ + getPublicKey: function () { + return window.checkoutConfig.payment[this.getCode()].publicKey; + }, + + /** + * @return {exports} + */ + initObservable: function () { + this._super() + .observe(['paymentReady']); + + this.grandTotalAmount = quote.totals()['base_grand_total']; + + quote.totals.subscribe(function () { + if (this.grandTotalAmount !== quote.totals()['base_grand_total']) { + this.grandTotalAmount = quote.totals()['base_grand_total']; + } + }, this); + + this.isInAction.subscribe(function (isInAction) { + this.processKbankPopup(isInAction); + }, this); + + return this; + }, + + /** + * Open popup for the payment + */ + processKbankPopup: function (isInAction) { + var self = this; + + if (!window.hasOwnProperty('KPayment')) { + return this; + } + + if (!this.isInitializedTrigger()) { + window.KPayment.onClose(function () { + $(document).trigger('ghoster:closeKbankPopup'); + }); + + this.isInitializedTrigger(true); + } + + if (isInAction) { + self.initKbankFormElement(); + window.KPayment.create(); + window.KPayment.setMid(window.checkoutConfig.payment[this.getCode()]['merchantId']); + window.KPayment.setPublickey(window.checkoutConfig.payment[this.getCode()]['publicKey']); + window.KPayment.setName(window.checkoutConfig.payment[this.getCode()]['shopName']); + window.KPayment.setAmount(quote.totals()['base_grand_total']); + window.KPayment.setCurrency(window.checkoutConfig.quoteData.quote_currency_code); + window.KPayment.setPaymentMethods('card'); + window.KPayment.show(); + + this.isPopupClicked(true); + } + + if (!isInAction + && this.isPopupClicked() + && !!this.orderId + ) { + return $.when( + validateOrderStatusAction(self.orderId) + ).done(function (response) { + if (!response) { + $.mage.redirect( + window.checkoutConfig.payment[self.getCode()]['failureUrl'] + ); + } + }).fail(function () { + $.mage.redirect( + window.checkoutConfig.payment[self.getCode()]['failureUrl'] + ); + }); + } + }, + + /** + * @return {*} + */ + isPaymentReady: function () { + return this.paymentReady(); + }, + + /** + * Places order in pending payment status. + */ + placePendingPaymentOrder: function () { + if (this.placeOrder()) { + fullScreenLoader.startLoader(); + this.isInAction(true); + } + }, + + /** + * Show popup after order placed + * @returns {*|boolean} + */ + getPlaceOrderDeferredObject: function () { + var self = this; + + return $.when( + placeOrderAction(this.getData(), this.messageContainer) + ).done( + function (orderId) { + if (!isNaN(orderId)) { + self.orderId = orderId; + self.isInAction(true); + } + fullScreenLoader.stopLoader(); + } + ).fail( + function () { + fullScreenLoader.stopLoader(); + self.isInAction(false); + }); + }, + + /** + * Hide loader when popup is fully loaded. + */ + popupLoaded: function () { + fullScreenLoader.stopLoader(); + }, + + + /** + * @return {Boolean} + */ + selectPaymentMethod: function () { + this.initKbankFormElement(); + return this._super(); + }, + + /** + * @return {Boolean} + */ + validate: function () { + return true; + } + }); + } +); diff --git a/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-installment-method.js b/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-installment-method.js new file mode 100644 index 0000000..d3c6dd9 --- /dev/null +++ b/view/frontend/web/js/view/payment/method-renderer/kbank-embedded-installment-method.js @@ -0,0 +1,390 @@ +define( + [ + 'Magento_Checkout/js/view/payment/default', + 'ko', + 'jquery', + 'underscore', + 'Magento_Checkout/js/action/place-order', + 'Magento_Checkout/js/model/full-screen-loader', + 'mage/template', + 'text!GhoSter_KbankPayments/template/payment/form/embedded.html', + 'Magento_Checkout/js/model/quote', + 'GhoSter_KbankPayments/js/model/alert', + 'GhoSter_KbankPayments/js/model/installment', + 'GhoSter_KbankPayments/js/action/validate-order-status', + 'Magento_Checkout/js/model/payment/additional-validators', + 'mageUtils', + 'uiLayout', + 'Magento_Ui/js/modal/modal', + 'mage/translate' + ], + function ( + Component, + ko, + $, + _, + placeOrderAction, + fullScreenLoader, + mageTemplate, + embeddedFormTemplate, + quote, + alert, + installment, + validateOrderStatusAction, + additionalValidators, + utils, + layout + ) { + 'use strict'; + + var defaultRendererTemplate = { + parent: '${ $.$data.parentName }', + name: '${ $.$data.name }', + component: 'GhoSter_KbankPayments/js/view/payment/kbank-embedded/installment-renderer/default', + provider: 'checkoutProvider' + }; + + return Component.extend({ + defaults: { + template: 'GhoSter_KbankPayments/payment/kbank-embedded-installment', + paymentReady: false, + inActionBodyClass: 'kbank-embedded-in-action', + grandTotalAmount: null, + embeddedFormPopup: null, + orderId: null, + isUseInstallment: false, + isPreDefinedInstallment: false, + selectedBankInstallment: installment.getInstallmentInformation()(), + rendererTemplates: [], + embeddedFormElmPrefix: 'kbank-embedded-', + embeddedFormElmPostfix: '-hidden' + }, + redirectAfterPlaceOrder: false, + isInitializedTrigger: ko.observable(false), + isPopupClicked: ko.observable(false), + isInAction: ko.observable(false), + + initialize: function () { + this._super(); + var context = this; + + $(document) + .on('ghoster:closeKbankPopup', function () { + context.isInAction(false); + }); + + this.isUseInstallment = window.checkoutConfig.payment[this.item.method]['isUseInstallment'] || false; + this.isPreDefinedInstallment = window.checkoutConfig.payment[this.item.method]['isPreDefinedInstallment'] || false; + this.initChildren(); + + }, + + /** @inheritdoc */ + initConfig: function () { + this._super(); + // the list of child components that are responsible for address rendering + this.rendererComponents = {}; + + return this; + }, + + /** @inheritdoc */ + initChildren: function () { + if (this.isUseInstallment && this.isPreDefinedInstallment) { + _.each(window.checkoutConfig.payment[this.item.method]['allowedInstallment'], this.createRendererComponent, this); + } + return this; + }, + + /** + * Initialize form elements + */ + initKbankFormElement: function () { + var script, + hiddenEmbeddedFormElmId = this.getHiddenButtonEmbeddedFormElmId(); + + script = document.querySelector('script[src^="' + this.getKbankSrc() + '"]'); + + if (!script) { + script = document.createElement('script'); + script.src = this.getKbankSrc(); + this.setDataForScript(script); + document.getElementById(hiddenEmbeddedFormElmId).appendChild(script); + } else { + this.setDataForScript(script); + } + }, + + /** + * Set data for script + * + * @param script + */ + setDataForScript: function (script) { + script.setAttribute('data-apikey', window.checkoutConfig.payment[this.getCode()].publicKey); + script.setAttribute('data-amount', quote.totals()['base_grand_total']); + script.setAttribute('data-currency', window.checkoutConfig.quoteData.quote_currency_code); + script.setAttribute('data-payment-methods', 'card'); + script.setAttribute('data-name', window.checkoutConfig.payment[this.getCode()]['shopName']); + script.setAttribute('data-mid', window.checkoutConfig.payment[this.getCode()]['merchantId']); + }, + + /** + * Get action url for payment method popup. + * @returns {String} + */ + getTokenProcessActionUrl: function () { + return window.checkoutConfig.payment[this.item.method]['processTokenActionUrl']; + }, + + /** + * @returns {String} + */ + getEmbeddedFormElmId: function () { + return this.embeddedFormElmPrefix + this.getCode(); + }, + + /** + * @returns {String} + */ + getHiddenButtonEmbeddedFormElmId: function () { + return this.embeddedFormElmPrefix + this.getCode() + this.embeddedFormElmPostfix; + }, + + /** + * @returns {String} + */ + getKbankSrc: function () { + return window.checkoutConfig.payment[this.getCode()].jsSrc; + }, + + /** + * @returns {String} + */ + getPublicKey: function () { + return window.checkoutConfig.payment[this.getCode()].publicKey; + }, + + /** + * Create new component that will render given installment in the address list + * + * @param {Object} installment + * @param {*} index + */ + createRendererComponent: function (installment, index) { + var rendererTemplate, templateData, rendererComponent, paymentMethodCode; + + paymentMethodCode = this.item.method; + + if (index in this.rendererComponents) { + this.rendererComponents[index].installment(installment); + } else { + // rendererTemplates are provided via layout + rendererTemplate = defaultRendererTemplate; + + templateData = { + parentName: this.name, + name: index + }; + rendererComponent = utils.template(rendererTemplate, templateData); + utils.extend(rendererComponent, { + installment: ko.observable(installment), + paymentMethodCode: ko.observable(paymentMethodCode) + + }); + layout([rendererComponent]); + this.rendererComponents[index] = rendererComponent; + } + }, + + /** + * @return {exports} + */ + initObservable: function () { + this._super() + .observe(['paymentReady', 'selectedBankInstallment']); + + this.grandTotalAmount = quote.totals()['base_grand_total']; + + quote.totals.subscribe(function () { + if (this.grandTotalAmount !== quote.totals()['base_grand_total']) { + this.grandTotalAmount = quote.totals()['base_grand_total']; + } + }, this); + + installment.installmentInformation.subscribe(function (installmentData) { + this.selectedBankInstallment(installmentData); + }, this); + + this.isInAction.subscribe(function (isInAction) { + this.processKbankPopup(isInAction); + }, this); + + return this; + }, + + /** + * Open popup for the payment + */ + processKbankPopup: function (isInAction) { + var self = this; + + if (!window.hasOwnProperty('KPayment')) { + return this; + } + + if (!this.isInitializedTrigger()) { + window.KPayment.onClose(function () { + $(document).trigger('ghoster:closeKbankPopup'); + }); + + this.isInitializedTrigger(true); + } + + if (isInAction) { + self.initKbankFormElement(); + window.KPayment.create(); + window.KPayment.setMid(window.checkoutConfig.payment[this.getCode()]['merchantId']); + window.KPayment.setPublickey(window.checkoutConfig.payment[this.getCode()]['publicKey']); + window.KPayment.setName(window.checkoutConfig.payment[this.getCode()]['shopName']); + window.KPayment.setAmount(quote.totals()['base_grand_total']); + window.KPayment.setCurrency(window.checkoutConfig.quoteData.quote_currency_code); + window.KPayment.setPaymentMethods('card'); + + var selectedBankInstallment = this.selectedBankInstallment(); + + if (this.isUseInstallment && this.isPreDefinedInstallment && !_.isEmpty(selectedBankInstallment)) { + window.KPayment.setSmartpayId(selectedBankInstallment['smartpay_id']); + window.KPayment.setTerm(selectedBankInstallment['payment_term']); + } + + window.KPayment.show(); + + this.isPopupClicked(true); + } + + if (!isInAction + && this.isPopupClicked() + && !!this.orderId + ) { + return $.when( + validateOrderStatusAction(self.orderId) + ).done(function (response) { + if (!response) { + $.mage.redirect( + window.checkoutConfig.payment[self.getCode()]['failureUrl'] + ); + } + }).fail(function () { + $.mage.redirect( + window.checkoutConfig.payment[self.getCode()]['failureUrl'] + ); + }); + } + }, + + /** + * @returns {Object} + */ + getData: function () { + var self = this, + selectedBankInstallment = this.selectedBankInstallment(); + + return { + method: this.getCode(), + 'additional_data': { + smartpay_id: self.isUseInstallment && self.isPreDefinedInstallment + ? selectedBankInstallment['smartpay_id'] + : '', + term: self.isUseInstallment && self.isPreDefinedInstallment + ? selectedBankInstallment['payment_term'] + : '', + terminal_id: self.isUseInstallment && self.isPreDefinedInstallment + ? window.checkoutConfig.payment[self.getCode()]['terminalId'] + : '' + } + }; + }, + + /** + * @return {*} + */ + isPaymentReady: function () { + return this.paymentReady(); + }, + + /** + * Places order in pending payment status. + */ + placePendingPaymentOrder: function () { + if (!this.validate()) { + alert({content: $.mage.__('Please specify installment option')}); + return false; + } + + if (this.placeOrder()) { + fullScreenLoader.startLoader(); + this.isInAction(true); + } + }, + + /** + * Show popup after order placed + * @returns {*|boolean} + */ + getPlaceOrderDeferredObject: function () { + var self = this; + + return $.when( + placeOrderAction(this.getData(), this.messageContainer) + ).done( + function (orderId) { + if (!isNaN(orderId)) { + self.orderId = orderId; + self.isInAction(true); + } + fullScreenLoader.stopLoader(); + } + ).fail( + function () { + fullScreenLoader.stopLoader(); + self.isInAction(false); + }); + }, + + /** + * Hide loader when popup is fully loaded. + */ + popupLoaded: function () { + fullScreenLoader.stopLoader(); + }, + + /** + * + * @returns {*} + */ + getAllowedInstallments: function () { + return this.rendererComponents; + }, + + /** + * @return {Boolean} + */ + selectPaymentMethod: function () { + this.initKbankFormElement(); + return this._super(); + }, + + /** + * @return {Boolean} + */ + validate: function () { + if (!this.isUseInstallment || !this.isPreDefinedInstallment) { + return true; + } + + return !_.isEmpty(this.selectedBankInstallment()['smartpay_id']); + } + }); + } +); diff --git a/view/frontend/web/template/payment/form/embedded.html b/view/frontend/web/template/payment/form/embedded.html new file mode 100644 index 0000000..cca972a --- /dev/null +++ b/view/frontend/web/template/payment/form/embedded.html @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/view/frontend/web/template/payment/installment-renderer/default.html b/view/frontend/web/template/payment/installment-renderer/default.html new file mode 100644 index 0000000..12f6fc1 --- /dev/null +++ b/view/frontend/web/template/payment/installment-renderer/default.html @@ -0,0 +1,15 @@ +
+ +
+ +
+
diff --git a/view/frontend/web/template/payment/kbank-embedded-fullpayment.html b/view/frontend/web/template/payment/kbank-embedded-fullpayment.html new file mode 100644 index 0000000..5aa40be --- /dev/null +++ b/view/frontend/web/template/payment/kbank-embedded-fullpayment.html @@ -0,0 +1,64 @@ +
+
+ + +
+
+ + + + +
+ + + +
+
+ + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + +
+
+
diff --git a/view/frontend/web/template/payment/kbank-embedded-installment.html b/view/frontend/web/template/payment/kbank-embedded-installment.html new file mode 100644 index 0000000..d93e8d0 --- /dev/null +++ b/view/frontend/web/template/payment/kbank-embedded-installment.html @@ -0,0 +1,72 @@ +
+
+ + +
+
+ + + + +
+
+ + + +
+
+ +
+ + + +
+
+ + + +
+ +
+ +
+ +
+
+ +
+
+
+ + + + +
+
+