19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:29:53 +01:00
parent 6e54c1af6c
commit 3ca647e428
1087 changed files with 132065 additions and 108499 deletions

View file

@ -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;
},
});

View file

@ -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
);
});
},
});

View file

@ -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();
}
});
},
});

View file

@ -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);

View file

@ -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;
});

View file

@ -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);
});

View file

@ -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;
});

View file

@ -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(),
});

View file

@ -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"];
},
});