Skip to content

Commit bf2a73b

Browse files
ArlindCopilot
andcommitted
Improve VoP polling flow and TAN response detection in approve_vop_response
1. Implement VoP polling (FinTS spec E.8.3.1): when the bank returns HIVPP with a polling_id but no vop_id, re-send HKVPP with polling_id + aufsetzpunkt (from HIRMS 3040) until the VoP check resolves and a vop_id is returned. 2. Broaden 3945 response code detection in VoP flow: check all HIRMG/HIRMS segments, not just tan_seg responses, since some banks attach it to different segments. 3. Add TAN fallback in approve_vop_response: after VoP approval, check command_seg and global HIRMG/HIRMS segments for 0030/3955 TAN-required codes (mirrors Fix 4 from PR raphaelm#210 but in the VoP approval path). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ade5323 commit bf2a73b

1 file changed

Lines changed: 76 additions & 4 deletions

File tree

fints/client.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
PinTanTwoStepAuthenticationMechanism,
2929
)
3030
from .segments.accounts import HISPA1, HKSPA1
31-
from .segments.auth import HIPINS1, HKTAB4, HKTAB5, HKTAN2, HKTAN3, HKTAN5, HKTAN6, HKTAN7, HIVPPS1, HIVPP1, PSRD1, HKVPA1
31+
from .segments.auth import HIPINS1, HKTAB4, HKTAB5, HKTAN2, HKTAN3, HKTAN5, HKTAN6, HKTAN7, HIVPPS1, HIVPP1, HKVPP1, PSRD1, HKVPA1
3232
from .segments.bank import HIBPA3, HIUPA4, HKKOM4
3333
from .segments.debit import (
3434
HKDBS1, HKDBS2, HKDMB1, HKDMC1, HKDME1, HKDME2,
@@ -1447,11 +1447,16 @@ def _send_pay_with_possible_retry(self, dialog, command_seg, resume_func):
14471447
- 'RVNM' - no match, no extra info seen
14481448
- 'RVNA' - check not available, reason in single_vop_result.na_reason
14491449
- 'PDNG' - pending, seems related to something not implemented right now.
1450+
1451+
VoP polling flow (FinTS spec E.8.3.1):
1452+
Some banks return HIVPP with no vop_id but a polling_id and code 3040:aufsetzpunkt.
1453+
The client must poll by re-sending HKVPP with polling_id + aufsetzpunkt (without
1454+
HKCCS/HKTAN) until the bank returns HIVPP with a vop_id and the actual VoP result.
1455+
After that, the client sends HKVPA + HKCCS + HKTAN to authorize.
14501456
"""
14511457
vop_seg = []
14521458
vop_standard = self._find_vop_format_for_segment(command_seg)
14531459
if vop_standard:
1454-
from .segments.auth import HKVPP1
14551460
vop_seg = [HKVPP1(supported_reports=PSRD1(psrd=[vop_standard]))]
14561461

14571462
with dialog:
@@ -1464,9 +1469,52 @@ def _send_pay_with_possible_retry(self, dialog, command_seg, resume_func):
14641469
if vop_standard:
14651470
hivpp = response.find_segment_first(HIVPP1, throw=True)
14661471

1472+
# Check if VOP polling is required: HIVPP has no vop_id but has polling_id
1473+
if not hivpp.vop_id and hivpp.polling_id:
1474+
# Extract aufsetzpunkt from HIRMS 3040 response
1475+
aufsetzpunkt = None
1476+
for hirms_seg in response.find_segments(HIRMS2):
1477+
for resp in hirms_seg.responses:
1478+
if resp.code == '3040' and resp.parameters:
1479+
aufsetzpunkt = resp.parameters[0]
1480+
1481+
wait_seconds = int(hivpp.wait_for_seconds) if hivpp.wait_for_seconds else 2
1482+
logger.info("VoP polling required (polling_id=%r, aufsetzpunkt=%r, wait=%ds)",
1483+
hivpp.polling_id, aufsetzpunkt, wait_seconds)
1484+
1485+
import time
1486+
time.sleep(wait_seconds)
1487+
1488+
# Poll: send HKVPP with polling_id + aufsetzpunkt (no HKCCS, no HKTAN)
1489+
poll_seg = HKVPP1(
1490+
supported_reports=PSRD1(psrd=[vop_standard]),
1491+
polling_id=hivpp.polling_id,
1492+
aufsetzpunkt=aufsetzpunkt,
1493+
)
1494+
poll_response = dialog.send(poll_seg)
1495+
hivpp = poll_response.find_segment_first(HIVPP1, throw=True)
1496+
logger.info("VoP poll result: vop_id=%r", hivpp.vop_id)
1497+
14671498
vop_result = hivpp.vop_single_result
1468-
# Not Applicable, No Match, Close Match, or exact match but still requires confirmation
1469-
if vop_result.result in ('RVNA', 'RVNM', 'RVMC') or (vop_result.result == 'RCVC' and '3945' in [res.code for res in response.responses(tan_seg)]):
1499+
# Not Applicable, No Match, Close Match, or exact match but still requires confirmation
1500+
tan_codes = [res.code for res in response.responses(tan_seg)]
1501+
command_codes = [res.code for res in response.responses(command_seg)]
1502+
all_codes = []
1503+
for seg in response.find_segments((HIRMG2, HIRMS2)):
1504+
all_codes.extend(r.code for r in seg.responses)
1505+
1506+
# If we have a vop_id (from initial or polling), return NeedVOPResponse
1507+
# so the caller can inspect the result and then call approve_vop_response
1508+
if hivpp.vop_id:
1509+
return NeedVOPResponse(
1510+
vop_result=hivpp,
1511+
command_seg=command_seg,
1512+
resume_method=resume_func,
1513+
)
1514+
1515+
if vop_result and (vop_result.result in ('RVNA', 'RVNM', 'RVMC') or (
1516+
vop_result.result == 'RCVC' and '3945' in all_codes
1517+
)):
14701518
return NeedVOPResponse(
14711519
vop_result=hivpp,
14721520
command_seg=command_seg,
@@ -1536,6 +1584,30 @@ def approve_vop_response(self, challenge: NeedVOPResponse):
15361584
challenge.vop_result,
15371585
)
15381586

1587+
for resp in response.responses(challenge.command_seg):
1588+
if resp.code in ('0030', '3955'):
1589+
return NeedTANResponse(
1590+
challenge.command_seg,
1591+
response.find_segment_first('HITAN'),
1592+
challenge.resume_method,
1593+
self.is_challenge_structured(),
1594+
resp.code == '3955',
1595+
challenge.vop_result,
1596+
)
1597+
1598+
for seg in response.find_segments((HIRMG2, HIRMS2)):
1599+
for resp in seg.responses:
1600+
if resp.code not in ('0030', '3955'):
1601+
continue
1602+
return NeedTANResponse(
1603+
challenge.command_seg,
1604+
response.find_segment_first('HITAN'),
1605+
challenge.resume_method,
1606+
self.is_challenge_structured(),
1607+
resp.code == '3955',
1608+
challenge.vop_result,
1609+
)
1610+
15391611
resume_func = getattr(self, challenge.resume_method)
15401612
return resume_func(challenge.command_seg, response)
15411613

0 commit comments

Comments
 (0)