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('
%s', params.get('name'));
} else if (params.get('name') && params.get('value')) {
return acc + _.str.sprintf('
%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;
});