Compare commits
3 commits
40c92613d9
...
5b2202f7d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b2202f7d4 | ||
|
|
57bf688169 | ||
|
|
7ae10abda8 |
13 changed files with 763 additions and 23 deletions
|
|
@ -51,10 +51,12 @@ To check the three fixes on a PoS with a CashDro terminal:
|
||||||
due amount (fix 2).
|
due amount (fix 2).
|
||||||
#. The drawer must start the operation and ask for the amount, instead of the
|
#. The drawer must start the operation and ask for the amount, instead of the
|
||||||
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
|
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
|
||||||
#. With no CashDro at hand, the browser console tells fix 1 from fix 3 apart:
|
|
||||||
without this module the failure is a ``TypeError: Cannot read properties of
|
These three fixes are not enough on their own: the payment flow of the base
|
||||||
undefined (reading 'payment_method')``; with it, a network error against the
|
module is broken on 18.0 for reasons of its own, and it hides every failure —
|
||||||
configured host.
|
these ones included — behind that same dialog, without logging anything.
|
||||||
|
Install ``pos_payment_method_cashdro_flow_fix`` as well; its ``USAGE`` explains
|
||||||
|
how to read what actually went wrong.
|
||||||
|
|
||||||
The Python side is covered by the tests. The base module lives outside this
|
The Python side is covered by the tests. The base module lives outside this
|
||||||
repo, so the OCA sources have to be on the addons path (mounted by
|
repo, so the OCA sources have to be on the addons path (mounted by
|
||||||
|
|
@ -76,7 +78,7 @@ the credential fields that are missing from the loader, and the JavaScript
|
||||||
patches are idempotent with respect to the fixed upstream code.
|
patches are idempotent with respect to the fixed upstream code.
|
||||||
|
|
||||||
The JavaScript side can't be covered by automated tests, as it needs a CashDro
|
The JavaScript side can't be covered by automated tests, as it needs a CashDro
|
||||||
terminal answering on the network.
|
terminal answering on the network. See ``USAGE`` for the manual check.
|
||||||
|
|
||||||
Contributors
|
Contributors
|
||||||
============
|
============
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ To check the three fixes on a PoS with a CashDro terminal:
|
||||||
due amount (fix 2).
|
due amount (fix 2).
|
||||||
#. The drawer must start the operation and ask for the amount, instead of the
|
#. The drawer must start the operation and ask for the amount, instead of the
|
||||||
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
|
*"An error occurred while connecting to the cashdro"* dialog (fixes 1 and 3).
|
||||||
#. With no CashDro at hand, the browser console tells fix 1 from fix 3 apart:
|
|
||||||
without this module the failure is a ``TypeError: Cannot read properties of
|
These three fixes are not enough on their own: the payment flow of the base
|
||||||
undefined (reading 'payment_method')``; with it, a network error against the
|
module is broken on 18.0 for reasons of its own, and it hides every failure —
|
||||||
configured host.
|
these ones included — behind that same dialog, without logging anything.
|
||||||
|
Install ``pos_payment_method_cashdro_flow_fix`` as well; its ``USAGE`` explains
|
||||||
|
how to read what actually went wrong.
|
||||||
|
|
||||||
The Python side is covered by the tests. The base module lives outside this
|
The Python side is covered by the tests. The base module lives outside this
|
||||||
repo, so the OCA sources have to be on the addons path (mounted by
|
repo, so the OCA sources have to be on the addons path (mounted by
|
||||||
|
|
|
||||||
164
pos_payment_method_cashdro_flow_fix/README.rst
Normal file
164
pos_payment_method_cashdro_flow_fix/README.rst
Normal 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
|
||||||
2
pos_payment_method_cashdro_flow_fix/__init__.py
Normal file
2
pos_payment_method_cashdro_flow_fix/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Copyright 2026 - Today Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
22
pos_payment_method_cashdro_flow_fix/__manifest__.py
Normal file
22
pos_payment_method_cashdro_flow_fix/__manifest__.py
Normal 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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
* `Criptomart <https://criptomart.net>`_:
|
||||||
|
|
||||||
|
* Criptomart
|
||||||
54
pos_payment_method_cashdro_flow_fix/readme/DESCRIPTION.rst
Normal file
54
pos_payment_method_cashdro_flow_fix/readme/DESCRIPTION.rst
Normal 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.
|
||||||
24
pos_payment_method_cashdro_flow_fix/readme/ROADMAP.rst
Normal file
24
pos_payment_method_cashdro_flow_fix/readme/ROADMAP.rst
Normal 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.
|
||||||
53
pos_payment_method_cashdro_flow_fix/readme/USAGE.rst
Normal file
53
pos_payment_method_cashdro_flow_fix/readme/USAGE.rst
Normal 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.
|
||||||
|
|
@ -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 += `¶meters=${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();
|
||||||
|
},
|
||||||
|
});
|
||||||
52
pos_payment_method_cashdro_rounding/README_DEV.md
Normal file
52
pos_payment_method_cashdro_rounding/README_DEV.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# pos_payment_method_cashdro_rounding
|
||||||
|
|
||||||
|
Sends the CashDro the amount the customer is really expected to pay — the due
|
||||||
|
amount with the PoS **cash rounding** applied — instead of the plain due amount
|
||||||
|
the OCA module asks for.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
A CashDro can only give change with the coins it holds. When the PoS is
|
||||||
|
configured with a cash rounding (`pos.config.cash_rounding`, typically to 0.05
|
||||||
|
so that 1 and 2 cent coins are not needed), asking the drawer for the unrounded
|
||||||
|
due leaves it trying to return change it does not have.
|
||||||
|
|
||||||
|
Core already computes that amount: `PosOrder.getDefaultAmountDueToPayIn()` is
|
||||||
|
what a payment line defaults to when it is created. We simply reuse it, so the
|
||||||
|
drawer is asked for exactly what the payment screen shows.
|
||||||
|
|
||||||
|
`shouldRound()` decides whether the rounding applies at all — it takes
|
||||||
|
`only_round_cash_method` into account, and the CashDro method is a cash one, so
|
||||||
|
in practice it is always true when a cash rounding is configured.
|
||||||
|
|
||||||
|
## How
|
||||||
|
|
||||||
|
One single patch on `PaymentCashdro.prototype._cashdro_amount()`, the hook
|
||||||
|
`pos_payment_method_cashdro_flow_fix` extracted for this. Everything else of the
|
||||||
|
OCA payment flow (request, acknowledge, polling, error handling) is untouched.
|
||||||
|
|
||||||
|
Two things this deliberately does **not** do:
|
||||||
|
|
||||||
|
- It does not touch the **reversals**. There the amount to send is the one of
|
||||||
|
the line being given back, not the remaining of the order, and it is already
|
||||||
|
the rounded figure that was collected. Rounding it a second time against the
|
||||||
|
order total would ask the drawer to collect the total all over again.
|
||||||
|
- It does not touch `_cashdro_payment_url()`, which is what it patched until
|
||||||
|
`18.0.1.1.0`. That method is shared by the payment and the reversal paths,
|
||||||
|
which is exactly how the reversal bug above got in, and it collides with the
|
||||||
|
fix modules that also patch it — the outcome then depending on the asset load
|
||||||
|
order.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Needs `pos_payment_method_cashdro_flow_fix` for the `_cashdro_amount()` hook,
|
||||||
|
and through it `pos_payment_method_cashdro_fix`: without them the OCA module
|
||||||
|
does not work on 18.0 at all, so there is nothing to round.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
There are no automated tests — the flow needs a CashDro answering on the
|
||||||
|
network. To check it by hand, configure a cash rounding of 0.05 on the PoS, take
|
||||||
|
an order to a total that is not a multiple of 0.05 (e.g. 17.43) and confirm the
|
||||||
|
drawer asks for 17.45. See the `USAGE` of
|
||||||
|
`pos_payment_method_cashdro_flow_fix` for how to read a failure.
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
|
# Copyright Criptomart
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{
|
{
|
||||||
"name": "POS CashDro Rounding",
|
"name": "POS CashDro Rounding",
|
||||||
"version": "18.0.1.0.0",
|
"version": "18.0.1.1.0",
|
||||||
"summary": "Apply cash/currency rounding to CashDro payment request amount.",
|
"summary": "Apply cash/currency rounding to CashDro payment request amount.",
|
||||||
"category": "Point of Sale",
|
"category": "Point of Sale",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"author": "Criptomart",
|
"author": "Criptomart",
|
||||||
"depends": [
|
"depends": [
|
||||||
"point_of_sale",
|
|
||||||
"pos_payment_method_cashdro",
|
"pos_payment_method_cashdro",
|
||||||
|
"pos_payment_method_cashdro_flow_fix",
|
||||||
],
|
],
|
||||||
"assets": {
|
"assets": {
|
||||||
"point_of_sale._assets_pos": [
|
"point_of_sale._assets_pos": [
|
||||||
"pos_payment_method_cashdro_rounding/static/src/js/cashdro_rounding.esm.js",
|
"pos_payment_method_cashdro_rounding/static/src/js/cashdro_rounding.esm.js",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"installable": True,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,21 +17,29 @@ patch(PaymentCashdro.prototype, {
|
||||||
* really expected to pay: the due amount with the cash rounding applied,
|
* really expected to pay: the due amount with the cash rounding applied,
|
||||||
* which is the very same amount core defaults a payment line to.
|
* which is the very same amount core defaults a payment line to.
|
||||||
*
|
*
|
||||||
* Only the url builder is patched so the rest of the payment flow of the
|
* The hook belongs to `pos_payment_method_cashdro_flow_fix`, where the
|
||||||
* base module (request, acknowledge, polling and error handling) is kept.
|
* amount is computed. This module used to patch `_cashdro_payment_url`
|
||||||
|
* instead, which is shared with the reversals: there the amount to send is
|
||||||
|
* the one of the line being given back, not the remaining of the order, so
|
||||||
|
* reversing a paid order asked the drawer to collect the rounded total all
|
||||||
|
* over again.
|
||||||
*/
|
*/
|
||||||
_cashdro_payment_url(parameters) {
|
_cashdro_amount(line, reversal) {
|
||||||
const order = this.pos.get_order();
|
const order = line.pos_order_id;
|
||||||
const line = order.get_selected_paymentline();
|
// `shouldRound` and `getDefaultAmountDueToPayIn` came with the 18.0 tax
|
||||||
if (!line || !order.shouldRound(line.payment_method_id)) {
|
// totals rework. On a build that predates them, fall back instead of
|
||||||
return super._cashdro_payment_url(...arguments);
|
// breaking the payment with a TypeError the operator would read as one
|
||||||
|
// more connection error.
|
||||||
|
const rounds =
|
||||||
|
!reversal &&
|
||||||
|
typeof order.getDefaultAmountDueToPayIn === "function" &&
|
||||||
|
order.shouldRound?.(line.payment_method_id);
|
||||||
|
if (!rounds) {
|
||||||
|
return super._cashdro_amount(...arguments);
|
||||||
}
|
}
|
||||||
// CashDro treats decimals as positions in an integer, so we round the
|
// CashDro treats decimals as positions in an integer, so we round the
|
||||||
// product to avoid the trailing decimals of the floating point
|
// product to avoid the trailing decimals of the floating point
|
||||||
// computation or the drawer would reject our request.
|
// computation or the drawer would reject our request.
|
||||||
const amount = Math.round(
|
return Math.round(order.getDefaultAmountDueToPayIn(line.payment_method_id) * 100);
|
||||||
order.getDefaultAmountDueToPayIn(line.payment_method_id) * 100
|
|
||||||
);
|
|
||||||
return super._cashdro_payment_url({ ...parameters, amount });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue