addons-cm/pos_payment_method_cashdro_flow_fix/README.rst
GitHub Copilot 7ae10abda8
[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>
2026-08-31 17:43:25 +02:00

164 lines
7.7 KiB
ReStructuredText

==============================
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