diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_adyen/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pos_adyen/static')
| -rw-r--r-- | addons/pos_adyen/static/src/js/models.js | 8 | ||||
| -rw-r--r-- | addons/pos_adyen/static/src/js/payment_adyen.js | 325 |
2 files changed, 333 insertions, 0 deletions
diff --git a/addons/pos_adyen/static/src/js/models.js b/addons/pos_adyen/static/src/js/models.js new file mode 100644 index 00000000..c022ea7b --- /dev/null +++ b/addons/pos_adyen/static/src/js/models.js @@ -0,0 +1,8 @@ +odoo.define('pos_adyen.models', function (require) { +var models = require('point_of_sale.models'); +var PaymentAdyen = require('pos_adyen.payment'); + +models.register_payment_method('adyen', PaymentAdyen); +models.register_payment_method('odoo_adyen', PaymentAdyen); +models.load_fields('pos.payment.method', 'adyen_terminal_identifier'); +}); diff --git a/addons/pos_adyen/static/src/js/payment_adyen.js b/addons/pos_adyen/static/src/js/payment_adyen.js new file mode 100644 index 00000000..9c9c4316 --- /dev/null +++ b/addons/pos_adyen/static/src/js/payment_adyen.js @@ -0,0 +1,325 @@ +odoo.define('pos_adyen.payment', function (require) { +"use strict"; + +var core = require('web.core'); +var rpc = require('web.rpc'); +var PaymentInterface = require('point_of_sale.PaymentInterface'); +const { Gui } = require('point_of_sale.Gui'); + +var _t = core._t; + +var PaymentAdyen = PaymentInterface.extend({ + send_payment_request: function (cid) { + this._super.apply(this, arguments); + this._reset_state(); + return this._adyen_pay(); + }, + send_payment_cancel: function (order, cid) { + this._super.apply(this, arguments); + // set only if we are polling + this.was_cancelled = !!this.polling; + return this._adyen_cancel(); + }, + close: function () { + this._super.apply(this, arguments); + }, + + // private methods + _reset_state: function () { + this.was_cancelled = false; + this.last_diagnosis_service_id = false; + this.remaining_polls = 2; + clearTimeout(this.polling); + }, + + _handle_odoo_connection_failure: function (data) { + // handle timeout + var line = this.pos.get_order().selected_paymentline; + if (line) { + line.set_payment_status('retry'); + } + this._show_error(_t('Could not connect to the Odoo server, please check your internet connection and try again.')); + + return Promise.reject(data); // prevent subsequent onFullFilled's from being called + }, + + _call_adyen: function (data, operation) { + return rpc.query({ + model: 'pos.payment.method', + method: 'proxy_adyen_request', + args: [[this.payment_method.id], data, operation], + }, { + // When a payment terminal is disconnected it takes Adyen + // a while to return an error (~6s). So wait 10 seconds + // before concluding Odoo is unreachable. + timeout: 10000, + shadow: true, + }).catch(this._handle_odoo_connection_failure.bind(this)); + }, + + _adyen_get_sale_id: function () { + var config = this.pos.config; + return _.str.sprintf('%s (ID: %s)', config.display_name, config.id); + }, + + _adyen_common_message_header: function () { + var config = this.pos.config; + this.most_recent_service_id = Math.floor(Math.random() * Math.pow(2, 64)).toString(); // random ID to identify request/response pairs + this.most_recent_service_id = this.most_recent_service_id.substring(0, 10); // max length is 10 + + return { + 'ProtocolVersion': '3.0', + 'MessageClass': 'Service', + 'MessageType': 'Request', + 'SaleID': this._adyen_get_sale_id(config), + 'ServiceID': this.most_recent_service_id, + 'POIID': this.payment_method.adyen_terminal_identifier + }; + }, + + _adyen_pay_data: function () { + var order = this.pos.get_order(); + var config = this.pos.config; + var line = order.selected_paymentline; + var data = { + 'SaleToPOIRequest': { + 'MessageHeader': _.extend(this._adyen_common_message_header(), { + 'MessageCategory': 'Payment', + }), + 'PaymentRequest': { + 'SaleData': { + 'SaleTransactionID': { + 'TransactionID': order.uid, + 'TimeStamp': moment().format(), // iso format: '2018-01-10T11:30:15+00:00' + } + }, + 'PaymentTransaction': { + 'AmountsReq': { + 'Currency': this.pos.currency.name, + 'RequestedAmount': line.amount, + } + } + } + } + }; + + if (config.adyen_ask_customer_for_tip) { + data.SaleToPOIRequest.PaymentRequest.SaleData.SaleToAcquirerData = "tenderOption=AskGratuity"; + } + + return data; + }, + + _adyen_pay: function () { + var self = this; + + if (this.pos.get_order().selected_paymentline.amount < 0) { + this._show_error(_t('Cannot process transactions with negative amount.')); + return Promise.resolve(); + } + + + if (this.poll_response_error) { + this.poll_response_error = false; + return self._adyen_handle_response({}); + } + + var data = this._adyen_pay_data(); + + return this._call_adyen(data).then(function (data) { + return self._adyen_handle_response(data); + }); + }, + + _adyen_cancel: function (ignore_error) { + var self = this; + var previous_service_id = this.most_recent_service_id; + var header = _.extend(this._adyen_common_message_header(), { + 'MessageCategory': 'Abort', + }); + + var data = { + 'SaleToPOIRequest': { + 'MessageHeader': header, + 'AbortRequest': { + 'AbortReason': 'MerchantAbort', + 'MessageReference': { + 'MessageCategory': 'Payment', + 'ServiceID': previous_service_id, + } + }, + } + }; + + return this._call_adyen(data).then(function (data) { + + // Only valid response is a 200 OK HTTP response which is + // represented by true. + if (! ignore_error && data !== "ok") { + self._show_error(_t('Cancelling the payment failed. Please cancel it manually on the payment terminal.')); + } + }); + }, + + _convert_receipt_info: function (output_text) { + return output_text.reduce(function (acc, entry) { + var params = new URLSearchParams(entry.Text); + + if (params.get('name') && !params.get('value')) { + return acc + _.str.sprintf('<br/>%s', params.get('name')); + } else if (params.get('name') && params.get('value')) { + return acc + _.str.sprintf('<br/>%s: %s', params.get('name'), params.get('value')); + } + + return acc; + }, ''); + }, + + _poll_for_response: function (resolve, reject) { + var self = this; + if (this.was_cancelled) { + resolve(false); + return Promise.resolve(); + } + + return rpc.query({ + model: 'pos.payment.method', + method: 'get_latest_adyen_status', + args: [[this.payment_method.id], this._adyen_get_sale_id()], + }, { + timeout: 5000, + shadow: true, + }).catch(function (data) { + reject(); + self.poll_response_error = true; + return self._handle_odoo_connection_failure(data); + }).then(function (status) { + var notification = status.latest_response; + var last_diagnosis_service_id = status.last_received_diagnosis_id; + var order = self.pos.get_order(); + var line = order.selected_paymentline; + + + if (self.last_diagnosis_service_id != last_diagnosis_service_id) { + self.last_diagnosis_service_id = last_diagnosis_service_id; + self.remaining_polls = 2; + } else { + self.remaining_polls--; + } + + if (notification && notification.SaleToPOIResponse.MessageHeader.ServiceID == self.most_recent_service_id) { + var response = notification.SaleToPOIResponse.PaymentResponse.Response; + var additional_response = new URLSearchParams(response.AdditionalResponse); + + if (response.Result == 'Success') { + var config = self.pos.config; + var payment_response = notification.SaleToPOIResponse.PaymentResponse; + var payment_result = payment_response.PaymentResult; + + var cashier_receipt = payment_response.PaymentReceipt.find(function (receipt) { + return receipt.DocumentQualifier == 'CashierReceipt'; + }); + + if (cashier_receipt) { + line.set_cashier_receipt(self._convert_receipt_info(cashier_receipt.OutputContent.OutputText)); + } + + var customer_receipt = payment_response.PaymentReceipt.find(function (receipt) { + return receipt.DocumentQualifier == 'CustomerReceipt'; + }); + + if (customer_receipt) { + line.set_receipt_info(self._convert_receipt_info(customer_receipt.OutputContent.OutputText)); + } + + var tip_amount = payment_result.AmountsResp.TipAmount; + if (config.adyen_ask_customer_for_tip && tip_amount > 0) { + order.set_tip(tip_amount); + line.set_amount(payment_result.AmountsResp.AuthorizedAmount); + } + + line.transaction_id = additional_response.get('pspReference'); + line.card_type = additional_response.get('cardType'); + line.cardholder_name = additional_response.get('cardHolderName') || ''; + resolve(true); + } else { + var message = additional_response.get('message'); + self._show_error(_.str.sprintf(_t('Message from Adyen: %s'), message)); + + // this means the transaction was cancelled by pressing the cancel button on the device + if (message.startsWith('108 ')) { + resolve(false); + } else { + line.set_payment_status('retry'); + reject(); + } + } + } else if (self.remaining_polls <= 0) { + self._show_error(_t('The connection to your payment terminal failed. Please check if it is still connected to the internet.')); + self._adyen_cancel(); + resolve(false); + } + }); + }, + + _adyen_handle_response: function (response) { + var line = this.pos.get_order().selected_paymentline; + + if (response.error && response.error.status_code == 401) { + this._show_error(_t('Authentication failed. Please check your Adyen credentials.')); + line.set_payment_status('force_done'); + return Promise.resolve(); + } + + response = response.SaleToPOIRequest; + if (response && response.EventNotification && response.EventNotification.EventToNotify == 'Reject') { + console.error('error from Adyen', response); + + var msg = ''; + if (response.EventNotification) { + var params = new URLSearchParams(response.EventNotification.EventDetails); + msg = params.get('message'); + } + + this._show_error(_.str.sprintf(_t('An unexpected error occured. Message from Adyen: %s'), msg)); + if (line) { + line.set_payment_status('force_done'); + } + + return Promise.resolve(); + } else { + line.set_payment_status('waitingCard'); + + var self = this; + var res = new Promise(function (resolve, reject) { + // clear previous intervals just in case, otherwise + // it'll run forever + clearTimeout(self.polling); + + self.polling = setInterval(function () { + self._poll_for_response(resolve, reject); + }, 3000); + }); + + // make sure to stop polling when we're done + res.finally(function () { + self._reset_state(); + }); + + return res; + } + }, + + _show_error: function (msg, title) { + if (!title) { + title = _t('Adyen Error'); + } + Gui.showPopup('ErrorPopup',{ + 'title': title, + 'body': msg, + }); + }, +}); + +return PaymentAdyen; +}); |
