odoo18/addons/pos_adyen/static/src/app/payment_adyen.js

306 lines
11 KiB
JavaScript

import { _t } from "@web/core/l10n/translation";
import { PaymentInterface } from "@point_of_sale/app/payment/payment_interface";
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { register_payment_method } from "@point_of_sale/app/store/pos_store";
import { sprintf } from "@web/core/utils/strings";
const { DateTime } = luxon;
export class PaymentAdyen extends PaymentInterface {
setup() {
super.setup(...arguments);
this.paymentLineResolvers = {};
}
send_payment_request(uuid) {
super.send_payment_request(uuid);
return this._adyen_pay(uuid);
}
send_payment_cancel(order, uuid) {
super.send_payment_cancel(order, uuid);
return this._adyen_cancel();
}
set_most_recent_service_id(id) {
this.most_recent_service_id = id;
}
pending_adyen_line() {
return this.pos.getPendingPaymentLine("adyen");
}
_handle_odoo_connection_failure(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(data, operation = false) {
return this.pos.data
.silentCall("pos.payment.method", "proxy_adyen_request", [
[this.payment_method_id.id],
data,
operation,
])
.catch(this._handle_odoo_connection_failure.bind(this));
}
_adyen_get_sale_id() {
var config = this.pos.config;
return sprintf("%s (ID: %s)", config.display_name, config.id);
}
_adyen_common_message_header() {
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_id.adyen_terminal_identifier,
};
}
_adyen_pay_data() {
var order = this.pos.get_order();
var config = this.pos.config;
var line = order.get_selected_paymentline();
var data = {
SaleToPOIRequest: {
MessageHeader: Object.assign(this._adyen_common_message_header(), {
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;
}
_adyen_pay(uuid) {
var order = this.pos.get_order();
if (order.get_selected_paymentline().amount < 0) {
this._show_error(_t("Cannot process transactions with negative amount."));
return Promise.resolve();
}
var data = this._adyen_pay_data();
var line = order.payment_ids.find((paymentLine) => paymentLine.uuid === uuid);
line.setTerminalServiceId(this.most_recent_service_id);
return this._call_adyen(data).then((data) => {
return this._adyen_handle_response(data);
});
}
_adyen_cancel(ignore_error) {
var config = this.pos.config;
var previous_service_id = this.most_recent_service_id;
var header = Object.assign(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((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."
)
);
}
});
}
_convert_receipt_info(output_text) {
return output_text.reduce((acc, entry) => {
var params = new URLSearchParams(entry.Text);
if (params.get("name") && !params.get("value")) {
return acc + sprintf("\n%s", params.get("name"));
} else if (params.get("name") && params.get("value")) {
return acc + sprintf("\n%s: %s", 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.
*/
_adyen_handle_response(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 false;
}
response = response.SaleToPOIRequest;
if (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(_t("An unexpected error occurred. Message from Adyen: %s", msg));
if (line) {
line.set_payment_status("force_done");
}
return false;
} else {
line.set_payment_status("waitingCard");
return this.waitForPaymentConfirmation();
}
}
waitForPaymentConfirmation() {
return new Promise((resolve) => {
this.paymentLineResolvers[this.pending_adyen_line().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._handle_odoo_connection_failure();
return;
}
const line = this.pending_adyen_line();
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(
sprintf(_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 handle_payment_response method on the payment line
const resolver = this.paymentLineResolvers?.[line.uuid];
if (resolver) {
resolver(isPaymentSuccessful);
} else {
line.handle_payment_response(isPaymentSuccessful);
}
}
isPaymentSuccessful(notification, response) {
return (
notification &&
notification.SaleToPOIResponse.MessageHeader.ServiceID ==
this.pending_adyen_line().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) => {
return receipt.DocumentQualifier == "CashierReceipt";
});
if (cashier_receipt) {
line.set_cashier_receipt(
this._convert_receipt_info(cashier_receipt.OutputContent.OutputText)
);
}
const customer_receipt = payment_response.PaymentReceipt.find((receipt) => {
return receipt.DocumentQualifier == "CustomerReceipt";
});
if (customer_receipt) {
line.set_receipt_info(
this._convert_receipt_info(customer_receipt.OutputContent.OutputText)
);
}
const tip_amount = payment_result.AmountsResp.TipAmount;
if (config.adyen_ask_customer_for_tip && tip_amount > 0) {
this.pos.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") || "";
}
_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);