[ADD] pos_payment_method_cashdro_flow_fix: fix the CashDro payment flow on 18.0

`_cashdro_payment_url()` builds the startOperation url with
`this.pos.pos_session.name`. 18.0 replaced that accessor by the `session`
getter of PosStore, so reading `.name` on the resulting `undefined` raises a
TypeError on every single payment, before any request leaves the browser.
`cashdro_send_payment_request` catches it with a bare `catch` that neither
logs nor rethrows and shows the generic "error connecting to the cashdro",
which is why the failure looked like a problem with the machine.

`pos_payment_method_cashdro_fix` only covers the data loading, so this goes
in a module of its own: it has a longer life than that temporary patch, and
it supersedes its `_cashdro_url()` override.

Along with the blocking one, the flow gets:

* the real error logged and shown in the dialog, so a wrong host, a busy
  drawer and a dead network stop being the same sentence,
* a missing host raised instead of the boolean `false` travelling into
  `fetch`, where it resolves against the Odoo server,
* `finishOperation` always sent on failure, or the drawer keeps the operation
  open and refuses every later payment of the session,
* polling on a 700ms interval with a 5 minute deadline, stopped by a cancel
  or by leaving the payment screen, instead of a tight unbounded loop,
* `send_payment_cancel()` returning whether the drawer took it, so the line
  no longer sticks in `waitingCard`,
* reversals sending the amount of the line negated: `get_due()` only
  discounts the payments already done, so the base module asked the drawer to
  collect the money a second time instead of dispensing it,
* the `uuid` of the line honoured instead of the selected one,
* the CashDro answers validated (HTTP status, JSON body, operation id).

`_cashdro_amount()` is split out as the single hook for the amount, which
`pos_payment_method_cashdro_rounding` uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-31 17:43:25 +02:00
parent 40c92613d9
commit 7ae10abda8
Signed by: snt
GPG key ID: A9FD34930EADBE71
8 changed files with 675 additions and 0 deletions

View file

@ -0,0 +1,164 @@
==============================
POS CashDro Payment Flow Fixes
==============================
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
|badge1| |badge2|
``pos_payment_method_cashdro_fix`` patches the three 17.0 leftovers that the
18.0 migration of ``pos_payment_method_cashdro`` left in the *data loading*.
This module takes care of the *payment flow* itself, which stays broken after
them.
**The blocking one.** ``_cashdro_payment_url()`` builds the ``startOperation``
url with ``this.pos.pos_session.name``. 18.0 replaced that accessor by the
``session`` getter of ``PosStore``, so ``this.pos.pos_session`` is ``undefined``
and reading ``.name`` on it raises a ``TypeError`` **on every single payment,
before any request leaves the browser**.
``cashdro_send_payment_request`` catches it with a bare ``catch`` that neither
logs nor rethrows, and shows *"An error occurred while connecting to the
cashdro"* — the same message a network failure or an unplugged drawer produce.
That is why the failure looks like a communication problem with the machine,
and why it happens with any other module of ours enabled or disabled.
**The rest.** Once the payment goes through, the flow still has these problems,
all of them fixed here:
#. **Failures are invisible.** The ``catch`` has no error binding, so nothing
reaches the browser console. Every cause — a wrong host, a busy drawer, a
bad password, an API mistake — reads as the same sentence. The error is now
logged and its message shown in the dialog.
#. **A missing host is not reported.** ``_cashdro_url()`` returns the boolean
``false``, which the callers concatenate into ``"false&operation=..."``;
``fetch`` resolves that against the Odoo server, gets a 404 page back and the
operator sees a connection error. It now raises with the method name.
#. **A started operation is never closed on failure.** If the acknowledge or the
polling fails after ``startOperation`` succeeded, the CashDro keeps the
operation open and refuses the next one: every later payment of the session
fails until the drawer is power cycled. ``finishOperation`` is now always
sent.
#. **The polling has no interval, no bound and no way to stop.** The base
implementation re-asks the drawer as fast as the network allows for as long
as the customer is inserting coins, and keeps going after a cancel. It is now
a 700 ms interval with a 5 minute deadline, stopped by a cancel or by leaving
the payment screen.
#. **A cancel is reported as failed.** ``send_payment_cancel()`` resolves to
``undefined``, which ``payment_screen`` reads as "the cancel failed" and
leaves the line stuck in ``waitingCard``, re-entering the cancel on every
click. It now returns whether the drawer took it.
#. **A reversal asks the drawer to collect the money again.**
``order.get_due()`` only discounts the payments that are already done, and
the line being reversed is not one of them, so the amount comes out positive
and the operation type is 4 (payment) instead of 3 (refund). The reversal
now uses the amount of the line, negated.
#. **The uuid of the line is ignored.** The three ``PaymentInterface`` entry
points receive the uuid of the line to act on and the base module works on
the selected one instead. The per-line *Send* and *Reverse* buttons do not
select the line first, so with two payment lines on the order the CashDro
works on the wrong one.
#. **The answers are not validated.** An HTTP error page or an envelope without
``data`` are taken as valid: the module acknowledges an empty operation id
and polls it forever. Both are now checked.
**Table of contents**
.. contents::
:local:
Usage
=====
There is nothing to configure: installing the module is enough.
Before anything else, check the payment method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18.0 added the ``payment_method_type`` field (*Integration*) to
``pos.payment.method``, and core clears ``use_payment_terminal`` whenever it is
saved as *None required*. On **Point of Sale ‣ Configuration ‣ Payment
Methods**, the CashDro method must have:
* **Integration** = *Terminal*
* **Integrate with** = *Cashdro*
* the terminal host, user and password filled in
With *Integration* left at *None required* the terminal is not wired at all:
clicking the payment method just adds a plain cash line, and nothing is ever
sent to the drawer — no dialog, no error, no traffic.
On site
~~~~~~~
#. Open a PoS session, add a product and go to the payment screen.
#. Select the CashDro payment method. The payment line must show **0**, not the
due amount (that one comes from ``pos_payment_method_cashdro_fix``).
#. The drawer must start the operation and ask for the amount.
If it still fails, the dialog now carries the reason instead of the generic
sentence, and the browser console has the full error under ``[CASHDRO]``. What
to expect:
``The CashDro payment method '...' has no terminal host configured.``
The credentials did not reach the front end. Either the fields are empty on
the payment method, or ``pos_payment_method_cashdro_fix`` is not installed.
``TypeError: Failed to fetch`` / ``NetworkError``
The browser cannot reach ``cashdro_host``. Check the host and that the till
is on the same network as the drawer. A PoS served over HTTPS also blocks a
plain ``http://`` drawer as mixed content, which shows up here.
``The CashDro answered 401/403/500 ...``
The drawer is reachable and answering: wrong user or password, or the
CashDro3WS service is failing.
``The CashDro did not return an operation id: ...``
The drawer refused ``startOperation``. Its own envelope is in the message —
usually a busy drawer with an operation still open from a previous failure.
``The CashDro did not finish the operation in time.``
``startOperation`` was accepted but the drawer never reached the finished
state within five minutes.
There are no automated tests: the flow needs a CashDro terminal answering on the
network, which is also why none of this is caught by the OCA CI.
Known issues / Roadmap
======================
Split from ``pos_payment_method_cashdro_fix`` on purpose: that module is a
temporary patch for three migration leftovers, meant to disappear as soon as
its ``upstream/*.patch`` is merged in ``OCA/pos``. This one has a longer life —
only the ``pos_session`` fix is a migration leftover, the rest are problems the
module has carried since 17.0 — so it should not go away with it.
It supersedes the ``_cashdro_url()`` patch of ``pos_payment_method_cashdro_fix``
(same method, resolved from the terminal instead of the selected line, raising
instead of returning ``false``). Once the upstream patch is merged, the
dependency has to be moved to ``pos_payment_method_cashdro`` and the
``_load_pos_data_fields`` override checked, as it is the only part of the other
module this one really needs.
Still open:
* The success path leaves the operation open on the drawer, as upstream does.
Whether the CashDro3WS expects a ``finishOperation`` after a finished
operation could not be checked without a machine, so the behaviour was left
untouched.
* ``_cashdro_url()`` does not escape the user and the password, again as
upstream does: credentials containing ``&``, ``=`` or ``#`` break the query
string.
* The whole thing wants a tour with a mocked CashDro3WS endpoint, which would
also let the OCA CI catch this class of breakage.
Contributors
============
* `Criptomart <https://criptomart.net>`_:
* Criptomart

View file

@ -0,0 +1,2 @@
# Copyright 2026 - Today Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

View file

@ -0,0 +1,22 @@
# Copyright 2026 - Today Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "POS CashDro Payment Flow Fixes",
"version": "18.0.1.0.0",
"summary": "Fix and harden the CashDro payment flow on 18.0: real errors, "
"bounded polling and correct reversals",
"category": "Point of Sale",
"license": "AGPL-3",
"author": "Criptomart",
"maintainers": ["Criptomart"],
"website": "https://git.criptomart.net/criptomart/addons-cm",
"depends": [
"pos_payment_method_cashdro",
"pos_payment_method_cashdro_fix",
],
"assets": {
"point_of_sale._assets_pos": [
"pos_payment_method_cashdro_flow_fix/static/src/js/payment_cashdro_flow.esm.js",
],
},
}

View file

@ -0,0 +1,3 @@
* `Criptomart <https://criptomart.net>`_:
* Criptomart

View file

@ -0,0 +1,54 @@
``pos_payment_method_cashdro_fix`` patches the three 17.0 leftovers that the
18.0 migration of ``pos_payment_method_cashdro`` left in the *data loading*.
This module takes care of the *payment flow* itself, which stays broken after
them.
**The blocking one.** ``_cashdro_payment_url()`` builds the ``startOperation``
url with ``this.pos.pos_session.name``. 18.0 replaced that accessor by the
``session`` getter of ``PosStore``, so ``this.pos.pos_session`` is ``undefined``
and reading ``.name`` on it raises a ``TypeError`` **on every single payment,
before any request leaves the browser**.
``cashdro_send_payment_request`` catches it with a bare ``catch`` that neither
logs nor rethrows, and shows *"An error occurred while connecting to the
cashdro"* — the same message a network failure or an unplugged drawer produce.
That is why the failure looks like a communication problem with the machine,
and why it happens with any other module of ours enabled or disabled.
**The rest.** Once the payment goes through, the flow still has these problems,
all of them fixed here:
#. **Failures are invisible.** The ``catch`` has no error binding, so nothing
reaches the browser console. Every cause — a wrong host, a busy drawer, a
bad password, an API mistake — reads as the same sentence. The error is now
logged and its message shown in the dialog.
#. **A missing host is not reported.** ``_cashdro_url()`` returns the boolean
``false``, which the callers concatenate into ``"false&operation=..."``;
``fetch`` resolves that against the Odoo server, gets a 404 page back and the
operator sees a connection error. It now raises with the method name.
#. **A started operation is never closed on failure.** If the acknowledge or the
polling fails after ``startOperation`` succeeded, the CashDro keeps the
operation open and refuses the next one: every later payment of the session
fails until the drawer is power cycled. ``finishOperation`` is now always
sent.
#. **The polling has no interval, no bound and no way to stop.** The base
implementation re-asks the drawer as fast as the network allows for as long
as the customer is inserting coins, and keeps going after a cancel. It is now
a 700 ms interval with a 5 minute deadline, stopped by a cancel or by leaving
the payment screen.
#. **A cancel is reported as failed.** ``send_payment_cancel()`` resolves to
``undefined``, which ``payment_screen`` reads as "the cancel failed" and
leaves the line stuck in ``waitingCard``, re-entering the cancel on every
click. It now returns whether the drawer took it.
#. **A reversal asks the drawer to collect the money again.**
``order.get_due()`` only discounts the payments that are already done, and
the line being reversed is not one of them, so the amount comes out positive
and the operation type is 4 (payment) instead of 3 (refund). The reversal
now uses the amount of the line, negated.
#. **The uuid of the line is ignored.** The three ``PaymentInterface`` entry
points receive the uuid of the line to act on and the base module works on
the selected one instead. The per-line *Send* and *Reverse* buttons do not
select the line first, so with two payment lines on the order the CashDro
works on the wrong one.
#. **The answers are not validated.** An HTTP error page or an envelope without
``data`` are taken as valid: the module acknowledges an empty operation id
and polls it forever. Both are now checked.

View file

@ -0,0 +1,24 @@
Split from ``pos_payment_method_cashdro_fix`` on purpose: that module is a
temporary patch for three migration leftovers, meant to disappear as soon as
its ``upstream/*.patch`` is merged in ``OCA/pos``. This one has a longer life —
only the ``pos_session`` fix is a migration leftover, the rest are problems the
module has carried since 17.0 — so it should not go away with it.
It supersedes the ``_cashdro_url()`` patch of ``pos_payment_method_cashdro_fix``
(same method, resolved from the terminal instead of the selected line, raising
instead of returning ``false``). Once the upstream patch is merged, the
dependency has to be moved to ``pos_payment_method_cashdro`` and the
``_load_pos_data_fields`` override checked, as it is the only part of the other
module this one really needs.
Still open:
* The success path leaves the operation open on the drawer, as upstream does.
Whether the CashDro3WS expects a ``finishOperation`` after a finished
operation could not be checked without a machine, so the behaviour was left
untouched.
* ``_cashdro_url()`` does not escape the user and the password, again as
upstream does: credentials containing ``&``, ``=`` or ``#`` break the query
string.
* The whole thing wants a tour with a mocked CashDro3WS endpoint, which would
also let the OCA CI catch this class of breakage.

View file

@ -0,0 +1,53 @@
There is nothing to configure: installing the module is enough.
Before anything else, check the payment method
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
18.0 added the ``payment_method_type`` field (*Integration*) to
``pos.payment.method``, and core clears ``use_payment_terminal`` whenever it is
saved as *None required*. On **Point of Sale ‣ Configuration ‣ Payment
Methods**, the CashDro method must have:
* **Integration** = *Terminal*
* **Integrate with** = *Cashdro*
* the terminal host, user and password filled in
With *Integration* left at *None required* the terminal is not wired at all:
clicking the payment method just adds a plain cash line, and nothing is ever
sent to the drawer — no dialog, no error, no traffic.
On site
~~~~~~~
#. Open a PoS session, add a product and go to the payment screen.
#. Select the CashDro payment method. The payment line must show **0**, not the
due amount (that one comes from ``pos_payment_method_cashdro_fix``).
#. The drawer must start the operation and ask for the amount.
If it still fails, the dialog now carries the reason instead of the generic
sentence, and the browser console has the full error under ``[CASHDRO]``. What
to expect:
``The CashDro payment method '...' has no terminal host configured.``
The credentials did not reach the front end. Either the fields are empty on
the payment method, or ``pos_payment_method_cashdro_fix`` is not installed.
``TypeError: Failed to fetch`` / ``NetworkError``
The browser cannot reach ``cashdro_host``. Check the host and that the till
is on the same network as the drawer. A PoS served over HTTPS also blocks a
plain ``http://`` drawer as mixed content, which shows up here.
``The CashDro answered 401/403/500 ...``
The drawer is reachable and answering: wrong user or password, or the
CashDro3WS service is failing.
``The CashDro did not return an operation id: ...``
The drawer refused ``startOperation``. Its own envelope is in the message —
usually a busy drawer with an operation still open from a previous failure.
``The CashDro did not finish the operation in time.``
``startOperation`` was accepted but the drawer never reached the finished
state within five minutes.
There are no automated tests: the flow needs a CashDro terminal answering on the
network, which is also why none of this is caught by the OCA CI.

View file

@ -0,0 +1,353 @@
/* Copyright 2026 - Today Criptomart
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
Payment flow of `pos_payment_method_cashdro` reworked for 18.0. See the
README for the reasoning behind each patch; the short version is that the
base module builds the *startOperation* url with the 17.0 session accessor,
which throws before any request leaves the browser, and then hides every
failure — that one included — behind the same generic dialog.
*/
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { PaymentCashdro } from "@pos_payment_method_cashdro/js/payment_cashdro.esm";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
// The CashDro answers `askOperation` with the state "F" once the operation is
// finished. Anything else means "keep asking", which we do on a timer instead
// of in a tight loop, and give up on after a while.
const POLL_INTERVAL_MS = 700;
const POLL_TIMEOUT_MS = 5 * 60 * 1000;
/**
* Raised when the polling is stopped on purpose (a cancel, or leaving the
* payment screen). It is not an error to report to the operator.
*/
class CashdroAborted extends Error {}
patch(PaymentCashdro.prototype, {
/**
* @override
*/
setup() {
super.setup(...arguments);
// Set while `_cashdro_request_payment` is asking the drawer, so that a
// cancel can actually stop it.
this.cashdro_polling = false;
},
// --------------------------------------------------------------------------
// Entry points of the PaymentInterface
//
// The three of them get the uuid of the line to act on, and the base module
// ignores it in favour of the selected one: the per-line Send/Reverse
// buttons don't select the line first, so with two payment lines on the
// order the CashDro ends up working on the wrong one. None of them can
// delegate to `super`, as the base body is what we are replacing.
// --------------------------------------------------------------------------
/**
* @override
*/
send_payment_request(uuid) {
const order = this.pos.get_order();
const line = this._cashdro_line(order, uuid);
line?.set_payment_status("waiting");
return this.cashdro_send_payment_request(order, line, false);
},
/**
* @override
*/
send_payment_reversal(uuid) {
const order = this.pos.get_order();
const line = this._cashdro_line(order, uuid);
line?.set_payment_status("reversing");
return this.cashdro_send_payment_request(order, line, true);
},
/**
* @override
* `payment_screen` reads the returned value: anything falsy means "the
* cancel failed" and leaves the line in `waitingCard` for good, with
* `deletePaymentLine` re-entering here on every click. The base method
* returns `undefined` even when the operation was finished just fine.
*/
async send_payment_cancel(order) {
// Stop the polling first: `cashdro_finish_operation` closes the
// operation on the drawer, and asking about it afterwards is pointless.
this.cashdro_polling = false;
const operation = (order || this.pos.get_order())?.cashdro_operation;
if (!operation) {
return true;
}
try {
await this.cashdro_finish_operation(operation);
} catch (error) {
console.error("[CASHDRO] finishOperation failed", error);
return false;
}
return true;
},
/**
* @override
* Leaving the payment screen has to stop the polling too, or it keeps
* hitting the drawer for the rest of the session.
*/
close() {
this.cashdro_polling = false;
return super.close(...arguments);
},
// --------------------------------------------------------------------------
// Payment flow
// --------------------------------------------------------------------------
/**
* @override
* Same three steps as the base module (startOperation, acknowledge, poll),
* with the failures made visible and the started operation always closed.
*
* @param {Object} order
* @param {Object} [line] the line to pay, defaults to the selected one
* @param {Boolean} [reversal]
* @returns {Promise<Boolean>}
*/
async cashdro_send_payment_request(order, line = null, reversal = false) {
const payment_line = line || order.get_selected_paymentline();
let operation_id = "";
try {
const amount = this._cashdro_amount(payment_line, reversal);
const res = await this._cashdro_request(this._cashdro_payment_url({ amount }));
operation_id = res.data || "";
if (!operation_id) {
// The drawer answers a refused operation (busy, bad
// credentials, unknown type) with an envelope carrying no
// data. Acknowledging and polling an empty id gets us nowhere.
throw new Error(
_t("The CashDro did not return an operation id: %s", JSON.stringify(res))
);
}
order.cashdro_operation = operation_id;
await this._cashdro_request(this._cashdro_ack_url(operation_id));
const operation_data = await this._cashdro_request_payment(
this._cashdro_ask_url(operation_id)
);
const data = JSON.parse(operation_data.data);
payment_line.cashdro_operation_data = data;
// `totalin` is what the customer put in the drawer, so it only says
// anything when we were collecting. On a reversal core zeroes the
// line itself, and on a refund the line already holds the amount
// the drawer was asked to give back.
if (!reversal && amount > 0) {
payment_line.set_amount(data.operation.totalin / 100);
}
} catch (error) {
if (error instanceof CashdroAborted) {
// `send_payment_cancel` already closed the operation and core
// takes care of the payment line.
return false;
}
console.error("[CASHDRO] payment request failed", error);
// An operation that was started and never finished keeps the drawer
// busy: it refuses the next `startOperation`, so every later
// payment of the session fails until it is power cycled.
if (operation_id) {
await this.cashdro_finish_operation(operation_id).catch((finish_error) =>
console.error("[CASHDRO] could not close the operation", finish_error)
);
}
payment_line?.set_payment_status("retry");
this.env.services.dialog.add(AlertDialog, {
title: _t("Error"),
body: _t(
"An error occurred while connecting to the cashdro.\n\n%s",
error.message || error
),
});
return false;
}
return true;
},
/**
* @override
* The order is not always around any more when the operation has to be
* closed (a cancel right after validating, for instance), and the caller
* wants to know whether the drawer took it.
*/
async cashdro_finish_operation(operation) {
if (!operation) {
return false;
}
const res = await this._cashdro_request(this._cashdro_finish_url(operation));
const order = this.pos.get_order();
if (order) {
order.cashdro_operation = false;
}
return res;
},
// --------------------------------------------------------------------------
// Amounts
// --------------------------------------------------------------------------
/**
* The amount, in cents, to ask the drawer for: positive collects money,
* negative gives it back. Split out of `cashdro_send_payment_request` so
* that `pos_payment_method_cashdro_rounding` has one single place to hook
* into, whatever the flow that reaches the drawer.
*
* @param {Object} line the payment line being paid or reversed
* @param {Boolean} reversal
* @returns {Number}
*/
_cashdro_amount(line, reversal) {
// `get_due()` only discounts the payments that are already `is_done()`,
// and `sendPaymentReverse` sets the line to "reversing" before getting
// here: the line being given back still counts as pending, so the due
// comes out positive and the base module asks the drawer to *collect*
// the amount a second time instead of dispensing it.
const amount = reversal ? -line.get_amount() : line.pos_order_id.get_due();
// The CashDro treats decimals as positions in an integer, and the
// floating point noise has to be absorbed or the drawer rejects the
// request.
return Math.round(amount * 100);
},
// --------------------------------------------------------------------------
// API communication
// --------------------------------------------------------------------------
/**
* @override
* Two changes over the base method (and over the one in
* `pos_payment_method_cashdro_fix`, which this one supersedes): the payment
* method comes from the terminal itself instead of the selected line —
* `send_payment_cancel` runs when the line is already deselected or
* removed — and a missing host is an error instead of the string "false"
* silently travelling into `fetch`, where it resolves against the Odoo
* server and looks exactly like a dead drawer.
*/
_cashdro_url() {
const method = this.payment_method_id;
if (!method?.cashdro_host) {
throw new Error(
_t(
"The CashDro payment method '%s' has no terminal host configured.",
method?.name || ""
)
);
}
// Cashdro machines don't support safe POST calls, so we're sending
// all the data quite unsafely constantly...
let url = `${method.cashdro_host}/Cashdro3WS/index.php`;
url += `?name=${method.cashdro_user}`;
url += `&password=${method.cashdro_password}`;
return url;
},
/**
* @override
* `this.pos.pos_session` is the 17.0 accessor, renamed in 18.0 to the
* `session` getter of `PosStore`. Reading `.name` on the resulting
* `undefined` throws a TypeError on every single payment, before any
* request reaches the drawer, and the base module turns it into the
* generic "error connecting to the cashdro" dialog.
*/
_cashdro_payment_url(parameters) {
const user = this.pos.get_cashier().id || this.pos.user.id;
// Type 4 for payment, 3 for refund
const operationType = parameters.amount > 0 ? 4 : 3;
parameters = { ...parameters, amount: Math.abs(parameters.amount) };
let url = `${this._cashdro_url()}&operation=startOperation&type=${operationType}`;
url += `&posid=pos-${this.pos.session.name}`;
url += `&posuser=${user}`;
url += `&parameters=${encodeURIComponent(JSON.stringify(parameters))}`;
return url;
},
/**
* @override
* The base method assumes any answer is a valid JSON envelope, so an HTTP
* error page comes back as a bare `SyntaxError` with no hint of what the
* drawer actually said.
*/
async _cashdro_request(url) {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
throw new Error(
_t("The CashDro answered %(status)s %(statusText)s", {
status: response.status,
statusText: response.statusText,
})
);
}
const body = await response.text();
try {
return JSON.parse(body);
} catch {
throw new Error(_t("Unexpected answer from the CashDro: %s", body.slice(0, 200)));
}
},
/**
* @override
* The base implementation re-asks the drawer with no delay, no bound and no
* way to stop: it floods the CashDro3WS endpoint for as long as the
* customer is inserting coins — which the embedded web service answers by
* dropping connections — and keeps polling a finished operation after a
* cancel.
*
* @param {String} request_url
* @returns {Promise}
*/
_cashdro_request_payment(request_url) {
this.cashdro_polling = true;
const deadline = Date.now() + POLL_TIMEOUT_MS;
return new Promise((resolve, reject) => {
const ask = async () => {
if (!this.cashdro_polling) {
return reject(new CashdroAborted("Polling stopped"));
}
if (Date.now() > deadline) {
this.cashdro_polling = false;
return reject(
new Error(_t("The CashDro did not finish the operation in time."))
);
}
try {
const responseData = await this._cashdro_request(request_url);
const data = JSON.parse(responseData.data);
if (data.operation.state === "F") {
this.cashdro_polling = false;
return resolve(responseData);
}
setTimeout(ask, POLL_INTERVAL_MS);
} catch (error) {
this.cashdro_polling = false;
reject(error);
}
};
ask();
});
},
// --------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------
/**
* The line the terminal has been asked to act on. 18.0 identifies it by
* uuid; the selected line is only a fallback for the base module's own
* calls.
*
* @param {Object} order
* @param {String} [uuid]
* @returns {Object}
*/
_cashdro_line(order, uuid) {
return (uuid && order.get_paymentline_by_uuid(uuid)) || order.get_selected_paymentline();
},
});