mirror of
https://github.com/bringout/oca-ocb-pos.git
synced 2026-04-24 11:22:01 +02:00
19.0 vanilla
This commit is contained in:
parent
6e54c1af6c
commit
3ca647e428
1087 changed files with 132065 additions and 108499 deletions
|
|
@ -0,0 +1,8 @@
|
|||
import { PosPayment } from "@point_of_sale/app/models/pos_payment";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(PosPayment.prototype, {
|
||||
setTerminalServiceId(id) {
|
||||
this.terminalServiceId = id;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { PaymentScreen } from "@point_of_sale/app/screens/payment_screen/payment_screen";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
|
||||
patch(PaymentScreen.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onMounted(() => {
|
||||
const pendingPaymentLine = this.currentOrder.payment_ids.find(
|
||||
(paymentLine) =>
|
||||
paymentLine.payment_method_id.use_payment_terminal === "adyen" &&
|
||||
!paymentLine.isDone() &&
|
||||
paymentLine.getPaymentStatus() !== "pending"
|
||||
);
|
||||
if (!pendingPaymentLine) {
|
||||
return;
|
||||
}
|
||||
pendingPaymentLine.payment_method_id.payment_terminal.setMostRecentServiceId(
|
||||
pendingPaymentLine.terminalServiceId
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
|
||||
patch(PosStore.prototype, {
|
||||
async setup() {
|
||||
await super.setup(...arguments);
|
||||
this.data.connectWebSocket("ADYEN_LATEST_RESPONSE", () => {
|
||||
const pendingLine = this.getPendingPaymentLine("adyen");
|
||||
|
||||
if (pendingLine) {
|
||||
pendingLine.payment_method_id.payment_terminal.handleAdyenStatusResponse();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { PaymentInterface } from "@point_of_sale/app/utils/payment/payment_interface";
|
||||
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { register_payment_method } from "@point_of_sale/app/services/pos_store";
|
||||
import { logPosMessage } from "@point_of_sale/app/utils/pretty_console_log";
|
||||
const { DateTime } = luxon;
|
||||
|
||||
export class PaymentAdyen extends PaymentInterface {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.paymentLineResolvers = {};
|
||||
}
|
||||
|
||||
sendPaymentRequest(uuid) {
|
||||
super.sendPaymentRequest(uuid);
|
||||
return this._adyenPay(uuid);
|
||||
}
|
||||
sendPaymentCancel(order, uuid) {
|
||||
super.sendPaymentCancel(order, uuid);
|
||||
return this._adyenCancel();
|
||||
}
|
||||
|
||||
setMostRecentServiceId(id) {
|
||||
this.most_recent_service_id = id;
|
||||
}
|
||||
|
||||
pendingAdyenline() {
|
||||
return this.pos.getPendingPaymentLine("adyen");
|
||||
}
|
||||
|
||||
_handleOdooConnectionFailure(data = {}) {
|
||||
// handle timeout
|
||||
var line = this.pendingAdyenline();
|
||||
if (line) {
|
||||
line.setPaymentStatus("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
|
||||
}
|
||||
|
||||
_callAdyen(data, operation = false) {
|
||||
return this.pos.data
|
||||
.silentCall("pos.payment.method", "proxy_adyen_request", [
|
||||
[this.payment_method_id.id],
|
||||
data,
|
||||
operation,
|
||||
])
|
||||
.catch(this._handleOdooConnectionFailure.bind(this));
|
||||
}
|
||||
|
||||
_adyenGetSaleId() {
|
||||
var config = this.pos.config;
|
||||
return `${config.display_name} (ID: ${config.id})`;
|
||||
}
|
||||
|
||||
_adyenCommonMessageHeader() {
|
||||
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._adyenGetSaleId(config),
|
||||
ServiceID: this.most_recent_service_id,
|
||||
POIID: this.payment_method_id.adyen_terminal_identifier,
|
||||
};
|
||||
}
|
||||
|
||||
_adyenPayData() {
|
||||
var order = this.pos.getOrder();
|
||||
var config = this.pos.config;
|
||||
var line = order.getSelectedPaymentline();
|
||||
var data = {
|
||||
SaleToPOIRequest: {
|
||||
MessageHeader: Object.assign(this._adyenCommonMessageHeader(), {
|
||||
MessageCategory: "Payment",
|
||||
}),
|
||||
PaymentRequest: {
|
||||
SaleData: {
|
||||
SaleTransactionID: {
|
||||
TransactionID: `${order.uuid}--${order.session_id.id}`,
|
||||
TimeStamp: DateTime.now().toFormat("yyyy-MM-dd'T'HH:mm:ssZZ"), // 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;
|
||||
}
|
||||
|
||||
_adyenPay(uuid) {
|
||||
var order = this.pos.getOrder();
|
||||
|
||||
if (order.getSelectedPaymentline().amount < 0) {
|
||||
this._show_error(_t("Cannot process transactions with negative amount."));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var data = this._adyenPayData();
|
||||
var line = order.payment_ids.find((paymentLine) => paymentLine.uuid === uuid);
|
||||
line.setTerminalServiceId(this.most_recent_service_id);
|
||||
return this._callAdyen(data).then((data) => this._adyenHandleResponse(data));
|
||||
}
|
||||
|
||||
_adyenCancel(ignore_error) {
|
||||
var config = this.pos.config;
|
||||
var previous_service_id = this.most_recent_service_id;
|
||||
var header = Object.assign(this._adyenCommonMessageHeader(), {
|
||||
MessageCategory: "Abort",
|
||||
});
|
||||
|
||||
var data = {
|
||||
SaleToPOIRequest: {
|
||||
MessageHeader: header,
|
||||
AbortRequest: {
|
||||
AbortReason: "MerchantAbort",
|
||||
MessageReference: {
|
||||
MessageCategory: "Payment",
|
||||
SaleID: this._adyenGetSaleId(config),
|
||||
ServiceID: previous_service_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return this._callAdyen(data).then((data) => {
|
||||
// Only valid response is a 200 OK HTTP response which is
|
||||
// represented by true.
|
||||
if (!ignore_error && data !== true) {
|
||||
this._show_error(
|
||||
_t(
|
||||
"Cancelling the payment failed. Please cancel it manually on the payment terminal."
|
||||
)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
_convertReceiptInfo(output_text) {
|
||||
return output_text.reduce((acc, entry) => {
|
||||
var params = new URLSearchParams(entry.Text);
|
||||
if (params.get("name") && !params.get("value")) {
|
||||
return acc + "\n" + params.get("name");
|
||||
} else if (params.get("name") && params.get("value")) {
|
||||
return `${acc}\n${params.get("name")}: ${params.get("value")}`;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the response that comes from Adyen
|
||||
* when we first make a request to pay.
|
||||
*/
|
||||
_adyenHandleResponse(response) {
|
||||
var line = this.pendingAdyenline();
|
||||
|
||||
if (!response || (response.error && response.error.status_code == 401)) {
|
||||
this._show_error(_t("Authentication failed. Please check your Adyen credentials."));
|
||||
line.setPaymentStatus("force_done");
|
||||
return false;
|
||||
}
|
||||
|
||||
response = response.SaleToPOIRequest;
|
||||
if (response?.EventNotification?.EventToNotify === "Reject") {
|
||||
logPosMessage("PaymentAdyen", "_adyenHandleResponse", `Error from Adyen`, false, [
|
||||
response,
|
||||
]);
|
||||
|
||||
var msg = "";
|
||||
if (response.EventNotification) {
|
||||
var params = new URLSearchParams(response.EventNotification.EventDetails);
|
||||
msg = params.get("message");
|
||||
}
|
||||
|
||||
this._show_error(_t("An unexpected error occurred. Message from Adyen: %s", msg));
|
||||
if (line) {
|
||||
line.setPaymentStatus("force_done");
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
line.setPaymentStatus("waitingCard");
|
||||
return this.waitForPaymentConfirmation();
|
||||
}
|
||||
}
|
||||
|
||||
waitForPaymentConfirmation() {
|
||||
return new Promise((resolve) => {
|
||||
this.paymentLineResolvers[this.pendingAdyenline().uuid] = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called from pos_bus when the payment
|
||||
* confirmation from Adyen is received via the webhook.
|
||||
*/
|
||||
async handleAdyenStatusResponse() {
|
||||
const notification = await this.pos.data.silentCall(
|
||||
"pos.payment.method",
|
||||
"get_latest_adyen_status",
|
||||
[[this.payment_method_id.id]]
|
||||
);
|
||||
|
||||
if (!notification) {
|
||||
this._handleOdooConnectionFailure();
|
||||
return;
|
||||
}
|
||||
const line = this.pendingAdyenline();
|
||||
const response = notification.SaleToPOIResponse.PaymentResponse.Response;
|
||||
const additional_response = new URLSearchParams(response.AdditionalResponse);
|
||||
const isPaymentSuccessful = this.isPaymentSuccessful(notification, response);
|
||||
if (isPaymentSuccessful) {
|
||||
this.handleSuccessResponse(line, notification, additional_response);
|
||||
} else {
|
||||
this._show_error(_t("Message from Adyen: %s", additional_response.get("message")));
|
||||
}
|
||||
// when starting to wait for the payment response we create a promise
|
||||
// that will be resolved when the payment response is received.
|
||||
// In case this resolver is lost ( for example on a refresh ) we
|
||||
// we use the handlePaymentResponse method on the payment line
|
||||
const resolver = this.paymentLineResolvers?.[line?.uuid];
|
||||
if (resolver) {
|
||||
resolver(isPaymentSuccessful);
|
||||
} else {
|
||||
line?.handlePaymentResponse(isPaymentSuccessful);
|
||||
}
|
||||
}
|
||||
isPaymentSuccessful(notification, response) {
|
||||
return (
|
||||
notification &&
|
||||
notification.SaleToPOIResponse.MessageHeader.ServiceID ==
|
||||
this.pendingAdyenline()?.terminalServiceId &&
|
||||
response.Result === "Success"
|
||||
);
|
||||
}
|
||||
handleSuccessResponse(line, notification, additional_response) {
|
||||
const config = this.pos.config;
|
||||
const payment_response = notification.SaleToPOIResponse.PaymentResponse;
|
||||
const payment_result = payment_response.PaymentResult;
|
||||
|
||||
const cashier_receipt = payment_response.PaymentReceipt.find(
|
||||
(receipt) => receipt.DocumentQualifier == "CashierReceipt"
|
||||
);
|
||||
|
||||
if (cashier_receipt) {
|
||||
line.setCashierReceipt(
|
||||
this._convertReceiptInfo(cashier_receipt.OutputContent.OutputText)
|
||||
);
|
||||
}
|
||||
|
||||
const customer_receipt = payment_response.PaymentReceipt.find(
|
||||
(receipt) => receipt.DocumentQualifier == "CustomerReceipt"
|
||||
);
|
||||
|
||||
if (customer_receipt) {
|
||||
line.setReceiptInfo(
|
||||
this._convertReceiptInfo(customer_receipt.OutputContent.OutputText)
|
||||
);
|
||||
}
|
||||
|
||||
const tip_amount = payment_result.AmountsResp.TipAmount;
|
||||
if (config.adyen_ask_customer_for_tip && tip_amount > 0) {
|
||||
this.pos.setTip(tip_amount);
|
||||
line.setAmount(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") || "";
|
||||
}
|
||||
|
||||
_show_error(msg, title) {
|
||||
if (!title) {
|
||||
title = _t("Adyen Error");
|
||||
}
|
||||
this.env.services.dialog.add(AlertDialog, {
|
||||
title: title,
|
||||
body: msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
register_payment_method("adyen", PaymentAdyen);
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
odoo.define('pos_adyen.PaymentScreen', function(require) {
|
||||
"use strict";
|
||||
|
||||
const PaymentScreen = require('point_of_sale.PaymentScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { onMounted } = owl;
|
||||
|
||||
const PosAdyenPaymentScreen = PaymentScreen => class extends PaymentScreen {
|
||||
setup() {
|
||||
super.setup();
|
||||
onMounted(() => {
|
||||
const pendingPaymentLine = this.currentOrder.paymentlines.find(
|
||||
paymentLine => paymentLine.payment_method.use_payment_terminal === 'adyen' &&
|
||||
(!paymentLine.is_done() && paymentLine.get_payment_status() !== 'pending')
|
||||
);
|
||||
if (pendingPaymentLine) {
|
||||
const paymentTerminal = pendingPaymentLine.payment_method.payment_terminal;
|
||||
paymentTerminal.set_most_recent_service_id(pendingPaymentLine.terminalServiceId);
|
||||
pendingPaymentLine.set_payment_status('waiting');
|
||||
paymentTerminal.start_get_status_polling().then(isPaymentSuccessful => {
|
||||
if (isPaymentSuccessful) {
|
||||
pendingPaymentLine.set_payment_status('done');
|
||||
pendingPaymentLine.can_be_reversed = paymentTerminal.supports_reversals;
|
||||
} else {
|
||||
pendingPaymentLine.set_payment_status('retry');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(PaymentScreen, PosAdyenPaymentScreen);
|
||||
|
||||
return PaymentScreen;
|
||||
});
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
odoo.define('pos_adyen.models', function (require) {
|
||||
const { register_payment_method, Payment } = require('point_of_sale.models');
|
||||
const PaymentAdyen = require('pos_adyen.payment');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
register_payment_method('adyen', PaymentAdyen);
|
||||
|
||||
const PosAdyenPayment = (Payment) => class PosAdyenPayment extends Payment {
|
||||
constructor(obj, options) {
|
||||
super(...arguments);
|
||||
this.terminalServiceId = this.terminalServiceId || null;
|
||||
}
|
||||
//@override
|
||||
export_as_JSON() {
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
json.terminal_service_id = this.terminalServiceId;
|
||||
return json;
|
||||
}
|
||||
//@override
|
||||
init_from_JSON(json) {
|
||||
super.init_from_JSON(...arguments);
|
||||
this.terminalServiceId = json.terminal_service_id;
|
||||
}
|
||||
setTerminalServiceId(id) {
|
||||
this.terminalServiceId = id;
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(Payment, PosAdyenPayment);
|
||||
});
|
||||
|
|
@ -1,333 +0,0 @@
|
|||
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(cid);
|
||||
},
|
||||
send_payment_cancel: function (order, cid) {
|
||||
this._super.apply(this, arguments);
|
||||
return this._adyen_cancel();
|
||||
},
|
||||
close: function () {
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
set_most_recent_service_id(id) {
|
||||
this.most_recent_service_id = id;
|
||||
},
|
||||
|
||||
pending_adyen_line() {
|
||||
return this.pos.get_order().paymentlines.find(
|
||||
paymentLine => paymentLine.payment_method.use_payment_terminal === 'adyen' && (!paymentLine.is_done()));
|
||||
},
|
||||
|
||||
// private methods
|
||||
_reset_state: function () {
|
||||
this.was_cancelled = false;
|
||||
this.remaining_polls = 4;
|
||||
clearTimeout(this.polling);
|
||||
},
|
||||
|
||||
_handle_odoo_connection_failure: function (data) {
|
||||
// handle timeout
|
||||
var line = this.pending_adyen_line();
|
||||
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 (cid) {
|
||||
var self = this;
|
||||
var order = this.pos.get_order();
|
||||
|
||||
if (order.selected_paymentline.amount < 0) {
|
||||
this._show_error(_t('Cannot process transactions with negative amount.'));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (order === this.poll_error_order) {
|
||||
delete this.poll_error_order;
|
||||
return self._adyen_handle_response({});
|
||||
}
|
||||
|
||||
var data = this._adyen_pay_data();
|
||||
var line = order.paymentlines.find(paymentLine => paymentLine.cid === cid);
|
||||
line.setTerminalServiceId(this.most_recent_service_id);
|
||||
return this._call_adyen(data).then(function (data) {
|
||||
return self._adyen_handle_response(data);
|
||||
});
|
||||
},
|
||||
|
||||
_adyen_cancel: function (ignore_error) {
|
||||
var self = this;
|
||||
var config = this.pos.config;
|
||||
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',
|
||||
'SaleID': this._adyen_get_sale_id(config),
|
||||
'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 !== true) {
|
||||
self._show_error(_t('Cancelling the payment failed. Please cancel it manually on the payment terminal.'));
|
||||
self.was_cancelled = !!self.polling;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_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('\n%s', params.get('name'));
|
||||
} else if (params.get('name') && params.get('value')) {
|
||||
return acc + _.str.sprintf('\n%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]],
|
||||
}, {
|
||||
timeout: 5000,
|
||||
shadow: true,
|
||||
}).catch(function (data) {
|
||||
if (self.remaining_polls != 0) {
|
||||
self.remaining_polls--;
|
||||
} else {
|
||||
reject();
|
||||
self.poll_error_order = self.pos.get_order();
|
||||
return self._handle_odoo_connection_failure(data);
|
||||
}
|
||||
// This is to make sure that if 'data' is not an instance of Error (i.e. timeout error),
|
||||
// this promise don't resolve -- that is, it doesn't go to the 'then' clause.
|
||||
return Promise.reject(data);
|
||||
}).then(function (status) {
|
||||
var notification = status.latest_response;
|
||||
var order = self.pos.get_order();
|
||||
var line = self.pending_adyen_line() || resolve(false);
|
||||
|
||||
if (notification && notification.SaleToPOIResponse.MessageHeader.ServiceID == line.terminalServiceId) {
|
||||
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 {
|
||||
line.set_payment_status('waitingCard')
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_adyen_handle_response: function (response) {
|
||||
var line = this.pending_adyen_line();
|
||||
|
||||
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 occurred. Message from Adyen: %s'), msg));
|
||||
if (line) {
|
||||
line.set_payment_status('force_done');
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
line.set_payment_status('waitingCard');
|
||||
return this.start_get_status_polling()
|
||||
}
|
||||
},
|
||||
|
||||
start_get_status_polling() {
|
||||
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._poll_for_response(resolve, reject);
|
||||
self.polling = setInterval(function () {
|
||||
self._poll_for_response(resolve, reject);
|
||||
}, 5500);
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/* global posmodel */
|
||||
import * as Chrome from "@point_of_sale/../tests/pos/tours/utils/chrome_util";
|
||||
import * as ReceiptScreen from "@point_of_sale/../tests/pos/tours/utils/receipt_screen_util";
|
||||
import * as PaymentScreen from "@point_of_sale/../tests/pos/tours/utils/payment_screen_util";
|
||||
import * as ProductScreen from "@point_of_sale/../tests/pos/tours/utils/product_screen_util";
|
||||
import * as Dialog from "@point_of_sale/../tests/generic_helpers/dialog_util";
|
||||
import { registry } from "@web/core/registry";
|
||||
const response_from_adyen_on_pos_webhook = (session, ServiceID) => ({
|
||||
SaleToPOIResponse: {
|
||||
MessageHeader: {
|
||||
MessageCategory: "Payment",
|
||||
MessageClass: "Service",
|
||||
MessageType: "Response",
|
||||
POIID: "my_adyen_terminal",
|
||||
ProtocolVersion: "3.0",
|
||||
SaleID: "Furniture Shop (ID: 1)",
|
||||
ServiceID,
|
||||
},
|
||||
PaymentResponse: {
|
||||
POIData: {
|
||||
POIReconciliationID: "1000",
|
||||
POITransactionID: {
|
||||
TimeStamp: "2024-10-24T11:24:30.020Z",
|
||||
TransactionID: "4eU8001729769070017.SD3Q9TMJJTSSM475",
|
||||
},
|
||||
},
|
||||
PaymentReceipt: [],
|
||||
PaymentResult: {
|
||||
AmountsResp: {
|
||||
AuthorizedAmount: 1.04,
|
||||
Currency: "USD",
|
||||
},
|
||||
CustomerLanguage: "en",
|
||||
OnlineFlag: true,
|
||||
PaymentAcquirerData: {
|
||||
AcquirerPOIID: "P400Plus-275319618",
|
||||
AcquirerTransactionID: {
|
||||
TimeStamp: "2024-10-24T11:24:30.020Z",
|
||||
TransactionID: "SD3Q9TMJJTSSM475",
|
||||
},
|
||||
ApprovalCode: "123456",
|
||||
MerchantID: "OdooMP_POS",
|
||||
},
|
||||
PaymentInstrumentData: {
|
||||
CardData: {
|
||||
CardCountryCode: "826",
|
||||
EntryMode: ["Contactless"],
|
||||
MaskedPan: "541333 **** 9999",
|
||||
PaymentBrand: "mc",
|
||||
SensitiveCardData: {
|
||||
CardSeqNumb: "33",
|
||||
ExpiryDate: "0228",
|
||||
},
|
||||
},
|
||||
PaymentInstrumentType: "Card",
|
||||
},
|
||||
},
|
||||
Response: {
|
||||
AdditionalResponse:
|
||||
"useless=true&metadata.pos_hmac=ba6c62413839eb32030a3ee6400af4d367b8fb889b54ea85dffcb5a13625c318",
|
||||
Result: "Success",
|
||||
},
|
||||
SaleData: {
|
||||
SaleTransactionID: {
|
||||
TimeStamp: "2024-10-24T11:24:29.000Z",
|
||||
TransactionID: `921e7aa8-36b3-400c-a416-2b9a1eaf1283--${session}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
registry.category("web_tour.tours").add("PosAdyenTour", {
|
||||
steps: () =>
|
||||
[
|
||||
Chrome.startPoS(),
|
||||
Dialog.confirm("Open Register"),
|
||||
ProductScreen.addOrderline("Desk Pad"),
|
||||
ProductScreen.clickPayButton(),
|
||||
PaymentScreen.clickPaymentMethod("Adyen"),
|
||||
{
|
||||
content: "Waiting for Adyen payment to be processed",
|
||||
trigger: ".electronic_status:contains('Waiting for card')",
|
||||
run: async function () {
|
||||
const payment_terminal =
|
||||
posmodel.getPendingPaymentLine("adyen").payment_method_id.payment_terminal;
|
||||
// The fact that we are shown the `Waiting for card` status means that the
|
||||
// request for payment has been sent to the adyen server ( in this case the mocked server )
|
||||
// and the server replied with an `ok` response.
|
||||
// As such, this is the time when we wait to receive the notification from adyen on the webhook
|
||||
// The simplest way to mock this notification is to send it ourselves here.
|
||||
|
||||
// ==> pretend to be adyen and send the notification to the POS
|
||||
const resp = await fetch("/pos_adyen/notification", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
response_from_adyen_on_pos_webhook(
|
||||
posmodel.config.current_session_id.id,
|
||||
payment_terminal.most_recent_service_id
|
||||
)
|
||||
),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new Error("Failed to notify Adyen webhook");
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
ReceiptScreen.isShown(),
|
||||
].flat(),
|
||||
});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosPaymentMethod } from "@point_of_sale/../tests/unit/data/pos_payment_method.data";
|
||||
|
||||
patch(PosPaymentMethod.prototype, {
|
||||
_load_pos_data_fields() {
|
||||
return [...super._load_pos_data_fields(), "adyen_terminal_identifier"];
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue