2828 PinTanTwoStepAuthenticationMechanism ,
2929)
3030from .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
3232from .segments .bank import HIBPA3 , HIUPA4 , HKKOM4
3333from .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