From d3c8633d182dd968cefbeb63d088cd1ef0b0c25d Mon Sep 17 00:00:00 2001 From: Hasan Hasanzade Date: Mon, 19 Jun 2023 13:52:59 +0400 Subject: [PATCH] Capture card and Automatic Capture during Document Creation for Invoice B2C and B2B (#72) --- composer.json | 2 +- ...rInvoiceDocumentCreatedEventSubscriber.php | 111 ++++++++++ .../src/capture/capture-card.html.twig | 81 +++++++ .../app/administration/src/capture/index.js | 204 ++++++++++++++++++ src/Resources/app/administration/src/main.js | 1 + .../app/administration/src/refund/index.js | 4 +- .../src/refund/refund-card.html.twig | 4 +- .../app/administration/src/snippet/de-DE.json | 30 +++ .../app/administration/src/snippet/en-GB.json | 30 +++ src/Resources/config/config.xml | 26 +++ src/Resources/config/services.xml | 8 + .../administration/js/better-payment.js | 2 +- src/Util/BetterPaymentClient.php | 17 ++ src/Util/ConfigReader.php | 2 + 14 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 src/EventSubscriber/OrderInvoiceDocumentCreatedEventSubscriber.php create mode 100644 src/Resources/app/administration/src/capture/capture-card.html.twig create mode 100644 src/Resources/app/administration/src/capture/index.js diff --git a/composer.json b/composer.json index 7b17cbf..b36e1a7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "better-payment/bp-plugin-shopware6-api2", "description": "Better Payment plugin to implement payment methods using API2", - "version": "1.0.0", + "version": "1.1.0", "type": "shopware-platform-plugin", "license": "proprietary", "authors": [ diff --git a/src/EventSubscriber/OrderInvoiceDocumentCreatedEventSubscriber.php b/src/EventSubscriber/OrderInvoiceDocumentCreatedEventSubscriber.php new file mode 100644 index 0000000..04ab71d --- /dev/null +++ b/src/EventSubscriber/OrderInvoiceDocumentCreatedEventSubscriber.php @@ -0,0 +1,111 @@ +betterPaymentClient = $betterPaymentClient; + $this->orderRepository = $orderRepository; + $this->configReader = $configReader; + } + + /** + * Returns the subscribed events and their corresponding methods. + * + * @return array The array of subscribed events and their methods. + */ + public static function getSubscribedEvents(): array + { + return [ + 'document.written' => 'onInvoiceDocumentCreated', + ]; + } + + /** + * Performs following actions when an invoice document is created: + * 1. confirms whether this is really invoice type document to go on + * 2. fetches order from DB, and its corresponding successful(last) order transaction + * 3. checks whether this order transaction is capturable + * 4. sends capture request using BetterPaymentClient + * + * @param EntityWrittenEvent $event + */ + public function onInvoiceDocumentCreated(EntityWrittenEvent $event): void + { + $payloads = $event->getPayloads(); + foreach ($payloads as $payload) { + if (isset($payload['config']['name']) && $payload['config']['name'] == 'invoice') { + $orderId = $payload['orderId']; + $criteria = new Criteria([$orderId]); + $criteria->addAssociation('transactions.paymentMethod'); + + /** @var OrderEntity $order */ + $order = $this->orderRepository->search( + $criteria, + Context::createDefaultContext() + )->first(); + + $orderTransaction = $order->getTransactions()->last(); + + if ($this->isCapturable($orderTransaction)) { + $invoiceId = $payload['config']['documentNumber']; + $invoiceDate = $payload['config']['documentDate']; + + $captureParameters = [ + 'transaction_id' => $orderTransaction->getCustomFields()['better_payment_transaction_id'], + 'invoice_id' => $invoiceId, + 'amount' => $order->getAmountTotal(), + 'execution_date' => $invoiceDate, + 'comment' => 'Captured using Shopware 6 plugin', + ]; + + $this->betterPaymentClient->capture($captureParameters); + } + } + } + } + + /** + * Checks if an order transaction is capturable based on the payment method and corresponding configuration flag. + * + * @param OrderTransactionEntity $orderTransaction + * @return bool Whether the order transaction is capturable or not. + */ + public function isCapturable(OrderTransactionEntity $orderTransaction): bool + { + $paymentMethodShortname = $orderTransaction->getPaymentMethod()->getCustomFields()['shortname']; + return ($paymentMethodShortname == Invoice::SHORTNAME && $this->configReader->getBool(ConfigReader::INVOICE_AUTOMATICALLY_CAPTURE_ON_ORDER_INVOICE_DOCUMENT_SENT)) + || ($paymentMethodShortname == InvoiceB2B::SHORTNAME && $this->configReader->getBool(ConfigReader::INVOICE_B2B_AUTOMATICALLY_CAPTURE_ON_ORDER_INVOICE_DOCUMENT_SENT)); + } +} \ No newline at end of file diff --git a/src/Resources/app/administration/src/capture/capture-card.html.twig b/src/Resources/app/administration/src/capture/capture-card.html.twig new file mode 100644 index 0000000..70f47f4 --- /dev/null +++ b/src/Resources/app/administration/src/capture/capture-card.html.twig @@ -0,0 +1,81 @@ +{% block sw_order_detail_base_line_items_card %} + {% parent %} + + + +{% endblock %} \ No newline at end of file diff --git a/src/Resources/app/administration/src/capture/index.js b/src/Resources/app/administration/src/capture/index.js new file mode 100644 index 0000000..f3df3d6 --- /dev/null +++ b/src/Resources/app/administration/src/capture/index.js @@ -0,0 +1,204 @@ +import template from './capture-card.html.twig'; +import whiteLabels from '../../../../data/whitelabels.json'; + +const {Component, Mixin, ApiService} = Shopware; + +Component.override('sw-order-detail-base', { + template, + + inject: [ + 'orderStateMachineService', + // 'acl', + ], + + mixins: [ + Mixin.getByName('notification'), + ], + + data() { + return { + capture: { + amount: null, + invoice_id: null, + comment: this.$tc('betterpayment.capture.defaults.comment'), + execution_date: null, + }, + captures: [], + processSuccess: false, + buttonDisabled: false, + + apiUrl: null, + apiAuth: null, + }; + }, + + created() { + this.setAPIProperties(); + }, + + computed: { + isBetterPaymentTransaction() { + return this.transaction.customFields !== null + && this.transaction.customFields.hasOwnProperty('better_payment_transaction_id'); + }, + + betterPaymentTransactionId() { + return this.isBetterPaymentTransaction ? this.transaction.customFields.better_payment_transaction_id : null; + }, + + isCapturablePaymentMethod() { + const capturablePaymentMethods = ['kar', 'kar_b2b']; + + return capturablePaymentMethods.includes(this.paymentMethod); + }, + + captureCardIsVisible() { + return this.isBetterPaymentTransaction && this.isCapturablePaymentMethod; + }, + + isCapturableState() { + const capturableStates = ['in_progress', 'paid_partially', 'paid']; + + return capturableStates.includes(this.transaction.stateMachineState.technicalName); + }, + + canCreateCapture() { + // TODO: add permission check here with AND + return this.isCapturableState; + }, + + paymentMethod() { + return this.transaction.paymentMethod.customFields.shortname; + } + }, + + watch: { + // when order is set get its transaction captures + // order is not directly set in created() lifecycle hook + order() { + if (this.captureCardIsVisible) { + this.getCaptures(); + } + } + }, + + methods: { + setAPIProperties() { + const pluginConfig = ApiService.getByName('systemConfigApiService'); + pluginConfig.getValues('BetterPayment').then(config => { + const environment = config['BetterPayment.config.environment']; + const whiteLabel = config['BetterPayment.config.whiteLabel']; + + const testAPIKey = config['BetterPayment.config.testAPIKey']; + const productionAPIKey = config['BetterPayment.config.productionAPIKey']; + const apiKey = environment === 'test' ? testAPIKey : productionAPIKey; + + const testOutgoingKey = config['BetterPayment.config.testOutgoingKey']; + const productionOutgoingKey = config['BetterPayment.config.productionOutgoingKey']; + const outgoingKey = environment === 'test' ? testOutgoingKey : productionOutgoingKey; + + this.apiUrl = whiteLabels[whiteLabel][environment].api_url; + this.apiAuth = btoa(apiKey + ':' + outgoingKey); + + return Promise.resolve(); + }); + }, + + getCaptures() { + const url = this.apiUrl + '/rest/transactions/' + this.betterPaymentTransactionId + '/log'; + + const headers = new Headers(); + headers.append('Authorization', 'Basic ' + this.apiAuth); + + const requestOptions = { + method: 'GET', + headers: headers, + }; + + fetch(url, requestOptions) + .then(response => response.json()) + .then(result => { + if (!result.hasOwnProperty('error_code')) { + this.captures = result.filter(log => log.type === 'capture') + .filter(log => [1,2,3].includes(log.status)); + } else { + this.createNotificationError({ + message: result.error_message + }); + } + }) + .catch(exception => { + this.createNotificationError({ + message: exception + }); + }); + }, + + createCapture() { + this.buttonDisabled = true; + const url = this.apiUrl + '/rest/capture'; + + const headers = new Headers(); + headers.append('Authorization', 'Basic ' + this.apiAuth); + headers.append('Content-Type', 'application/json'); + + const body = JSON.stringify({ + 'transaction_id': this.betterPaymentTransactionId, + 'amount': this.capture.amount, + 'invoice_id': this.capture.invoice_id, + 'comment': this.capture.comment, + 'execution_date': this.capture.execution_date + }); + + const requestOptions = { + method: 'POST', + headers: headers, + body: body + }; + + fetch(url, requestOptions) + .then(response => response.json()) + .then(result => { + this.buttonDisabled = false; + // detect capture api request error and show as notification + if (result.error_code === 0) { + // capture statuses can be success|started|local|error + // Note: All statuses except for "error" are considered to be successful. + if (result.status !== 'error') { + // update capture card table records + this.getCaptures(); + + // this is to show check mark on submit button + this.processSuccess = true; + + this.createNotificationSuccess({ + message: this.$tc('betterpayment.capture.messages.successfulCaptureRequest') + }); + } else { + this.createNotificationError({ + message: this.$tc('betterpayment.capture.messages.invalidCaptureRequest') + }); + } + } else { + this.createNotificationError({ + message: result.error_message + }); + } + }) + .catch(exception => { + this.createNotificationError({ + message: exception + }); + }); + }, + + createCaptureFinished() { + this.capture.amount = null; + this.capture.invoice_id = null; + this.capture.comment = this.$tc('betterpayment.capture.defaults.comment'); + this.capture.execution_date = null; + + this.processSuccess = false; + }, + }, +}); \ No newline at end of file diff --git a/src/Resources/app/administration/src/main.js b/src/Resources/app/administration/src/main.js index 5a0af5e..7a9ba4e 100644 --- a/src/Resources/app/administration/src/main.js +++ b/src/Resources/app/administration/src/main.js @@ -1 +1,2 @@ import './refund'; +import './capture'; diff --git a/src/Resources/app/administration/src/refund/index.js b/src/Resources/app/administration/src/refund/index.js index 7eb564b..0df6f75 100644 --- a/src/Resources/app/administration/src/refund/index.js +++ b/src/Resources/app/administration/src/refund/index.js @@ -45,7 +45,7 @@ Component.override('sw-order-detail-base', { return this.isBetterPaymentTransaction ? this.transaction.customFields.better_payment_transaction_id : null; }, - cardIsVisible() { + refundCardIsVisible() { return this.isBetterPaymentTransaction; }, @@ -73,7 +73,7 @@ Component.override('sw-order-detail-base', { // when order is set get its transaction refunds // order is not directly set in created() lifecycle hook order() { - if (this.cardIsVisible) { + if (this.refundCardIsVisible) { this.getRefunds(); } } diff --git a/src/Resources/app/administration/src/refund/refund-card.html.twig b/src/Resources/app/administration/src/refund/refund-card.html.twig index ade5d9e..596faf9 100644 --- a/src/Resources/app/administration/src/refund/refund-card.html.twig +++ b/src/Resources/app/administration/src/refund/refund-card.html.twig @@ -1,7 +1,7 @@ {% block sw_order_detail_base_line_items_card %} {% parent %}