Poll for asynchronous VoP results (Atruvia banks) - #223
Conversation
Some banks do not return the Verification of Payee result inline. Their HIVPP
segment carries neither vop_id nor vop_single_result, only a polling_id and
wait_for_seconds, accompanied by response codes 3905 ("no challenge created")
and 3040 with the touchdown point (Aufsetzpunkt).
python-fints only evaluates inline results and never polls, so these transfers
inevitably end in 3945 ("approval without VoP confirmation not possible") and
can never be executed. This affects all Atruvia-hosted banks (Volksbank, GLS,
VR), see raphaelm#220.
The data structures were already present (HKVPP1.polling_id,
HKVPP1.aufsetzpunkt, HIVPP1.polling_id) -- only the client-side loop was
missing.
Adds:
- _find_vop_aufsetzpunkt(): reads the touchdown point from HIRMS code 3040.
- _poll_vop_result(): re-sends HKVPP with polling_id AND aufsetzpunkt until a
result arrives. Both are mandatory; sending only one yields 9210.
Two deliberate design choices:
- The loop terminates on the presence of vop_id/payment_status_report rather
than on a specific response code. Depending on the server release, Atruvia
completes the check with either 3090 or 0020/0025, so checking for a code
would loop forever on the other variant.
- On timeout it raises FinTSClientError instead of falling through. Without a
result there is nothing to approve and the order was not executed; a caller
receiving a warnings-only TransactionResponse could mistake it for success.
Backwards compatibility is handled by flow type, not by the presence of vop_id:
only when polling actually happened does the code return NeedVOPResponse early.
Banks answering inline -- Sparkassen, which already fire the pushTAN in step 1
and reject a separate HKVPA with 9010 -- fall through to the unchanged branch
and keep their TAN detection.
Verified against Berliner Volksbank (BLZ 10090000, Atruvia). The polled result
arrived on the first attempt and the transfer was executed:
3945 Freigabe ohne VOP-Bestaetigung nicht moeglich
-> VoP polling attempt 1 (aufsetzpunkt='staticscrollref', wait=2s)
-> 0025 Keine Namensabweichung, vop_id set
-> 0020 Ausfuehrungsbestaetigung nach Namensabgleich erhalten
-> 0020 SEPA-Einzelueberweisung erfolgreich
Note: that test was a same-account transfer, which the bank waived SCA for
(3076). The TAN path for third-party payees is not yet covered.
|
@raphaelm — I've completed the half of the verification that was missing when I opened this, A transfer through a Hamburger Sparkasse account (inline VoP) on the patched version: No polling log line was emitted at all, so the new branch was never entered and the transfer The branch is selected by flow type ( One observation from the same run, unrelated to this PR but possibly useful: that successful Happy to adjust anything, split it up, or add tests if that helps. |
Fixes the asynchronous VoP flow reported in #220.
Summary
Some banks answer a VoP check later, not inline. They send a
polling_idand expectthe client to ask again. python-fints never asks again, so on those banks every transfer
fails with
3945and can never be executed.This adds the missing "ask again" loop — about 70 lines, confined to the VoP path.
Verified on both bank types:
The problem
On an affected bank,
HIVPPcomes back like this — novop_id, novop_single_result,just a ticket number and "ask me again in 2 seconds":
alongside
3905("no challenge created"),3040with the touchdown point, and3945("approval without VoP confirmation not possible").
_send_pay_with_possible_retry()only looks athivpp.vop_single_result, which is emptyhere, so it falls through and the order is never approved. Users in #220 report this for
Berliner Volksbank, Volksbank Dresden-Bautzen and Volksbank Bonn Rhein-Sieg.
The segment fields for polling already exist (
HKVPP1.polling_id,HKVPP1.aufsetzpunkt,HIVPP1.polling_id) — only the client-side loop was missing.The fix
Two new methods:
_find_vop_aufsetzpunkt()— reads the touchdown point out of response code3040._poll_vop_result()— re-sendsHKVPPwithpolling_idandaufsetzpunktuntil the bank returns a result. Both fields are required; sending only one yields
9210(independently confirmed by two reporters in VoP: asynchronous result (polling / Aufsetzpunkt) not handled — transfers fail with 3905/3945 at Atruvia banks (GLS/VR) #220).Why this cannot break banks that answer inline
This was my main concern while writing it, since @payne1979 pointed out in #220 that
Sparkassen answer inline, fire the pushTAN in step 1, and reject a separate
HKVPAwith9010— "Any fix that assumes one of the two will break the other."The branch is therefore selected by flow type, not by the presence of
vop_id:A bank that answers inline never has
polling_idwithoutvop_id, sopolledstaysFalseand execution continues into the existing branch untouched — including the0030/3955TAN detection.Measured, not assumed. A transfer through a Hamburger Sparkasse account after this
change produced:
with no polling log line at all — the new code was never entered, and the transfer went
through as before.
Verification on the asynchronous side
Berliner Volksbank (BLZ 10090000, Atruvia). The polled result arrived on the first attempt
and the transfer executed:
Bank parameters for that institution:
Two design decisions worth a look
Termination on data, not on a response code. The loop stops when
vop_idorpayment_status_reportappears. Depending on the server release, Atruvia finishes witheither
3090or0020/0025— both are reported in #220 — so waiting for one specificcode hangs on the other variant.
Timeout raises instead of falling through. Without a result there is nothing to approve
and the order was not executed. Falling through would hand the caller a
TransactionResponsecarrying only warnings, which reads like success — note that_RESPONSE_STATUS_MAPPINGmaps3945toWARNING, notERROR.Scope and limitations
3076).The TAN path is therefore not covered by them. Worth flagging for a follow-up:
send_tan()attachesHKVPAonly whenvop_single_result.result == 'RCVC', and thatfield is empty on a polled result. Not touched by this PR.
pain.002is passed through untouched, not parsed. Callers receive theHIVPPand caninspect the report before approving.
Relation to #211
#211 targets the same issue and was a useful starting point — credit to @ArlindNocaj for
the polling idea. This PR differs in three ways:
vop_id, which keeps the inline pathreachable (see above),
security.pyorformals.py.