@@ -225,10 +225,6 @@ def process_response_message(self, dialog, message: FinTSInstituteMessage, inter
225225 message .find_segments ('HIUPD' )
226226 )
227227
228- vpps = message .find_segment_first (HIVPPS1 )
229- if vpps :
230- self .vpps = vpps
231-
232228 for seg in message .find_segments (HIRMG2 ):
233229 for response in seg .responses :
234230 if not internal_send :
@@ -797,7 +793,6 @@ def _find_supported_sepa_version(self, candidate_versions):
797793 return candidate_versions [0 ]
798794
799795 bank_supported = list (hispas .parameter .supported_sepa_formats )
800- print (hispas )
801796
802797 for candidate in candidate_versions :
803798 if "urn:iso:std:iso:20022:tech:xsd:{}" .format (candidate ) in bank_supported :
@@ -823,7 +818,7 @@ def simple_sepa_transfer(self, account: SEPAAccount, iban: str, bic: str,
823818 :param reason: Transfer reason
824819 :param instant_payment: Whether to use instant payment (defaults to ``False``)
825820 :param endtoend_id: End-to-end-Id (defaults to ``NOTPROVIDED``)
826- :return: Returns either a NeedRetryResponse or TransactionResponse
821+ :return: Returns either a NeedRetryResponse or NeedVOPResponse or TransactionResponse
827822 """
828823 config = {
829824 "name" : account_name ,
@@ -1063,14 +1058,49 @@ def resume_dialog(self, dialog_data):
10631058 self ._standing_dialog = None
10641059
10651060
1061+ class NeedVOPResponse (NeedRetryResponse ):
1062+
1063+ def __init__ (self , vop_result , command_seg , resume_method = None ):
1064+ self .vop_result = vop_result
1065+ self .command_seg = command_seg
1066+ if hasattr (resume_method , '__func__' ):
1067+ self .resume_method = resume_method .__func__ .__name__
1068+ else :
1069+ self .resume_method = resume_method
1070+
1071+ def __repr__ (self ):
1072+ return '<o.__class__.__name__(vop_result={o.vop_result!r})>' .format (o = self )
1073+
1074+ @classmethod
1075+ def _from_data_v1 (cls , data ):
1076+ if data ["version" ] == 1 :
1077+ segs = SegmentSequence (data ['segments_bin' ]).segments
1078+ return cls (segs [0 ], segs [1 ], resume_method = data ['resume_method' ])
1079+
1080+ raise Exception ("Wrong blob data version" )
1081+
1082+ def get_data (self ) -> bytes :
1083+ """Return a compressed datablob representing this object.
1084+
1085+ To restore the object, use :func:`fints.client.NeedRetryResponse.from_data`.
1086+ """
1087+ data = {
1088+ "_class_name" : self .__class__ .__name__ ,
1089+ "version" : 1 ,
1090+ "segments_bin" : SegmentSequence ([self .vop_result , self .command_seg ]).render_bytes (),
1091+ "resume_method" : self .resume_method ,
1092+ }
1093+ return compress_datablob (DATA_BLOB_MAGIC_RETRY , 1 , data )
1094+
1095+
10661096class NeedTANResponse (NeedRetryResponse ):
10671097 challenge_raw = None #: Raw challenge as received by the bank
10681098 challenge = None #: Textual challenge to be displayed to the user
10691099 challenge_html = None #: HTML-safe challenge text, possibly with formatting
10701100 challenge_hhduc = None #: HHD_UC challenge to be transmitted to the TAN generator
10711101 challenge_matrix = None #: Matrix code challenge: tuple(mime_type, data)
10721102 decoupled = None #: Use decoupled process
1073- vop_result = None # VoC result to either send an accept reply with TAN (on full match) or display a warning (otherwise; reply already sent)
1103+ vop_result = None #: VoP result
10741104
10751105 def __init__ (self , command_seg , tan_request , resume_method = None , tan_request_structured = False , decoupled = False , vop_result = None ):
10761106 self .command_seg = command_seg
@@ -1307,12 +1337,16 @@ def _get_tan_segment(self, orig_seg, tan_process, tan_seg=None):
13071337 return seg
13081338
13091339 def _find_vop_format_for_segment (self , seg ):
1310- needed = str (seg .header .type ) in list (self .vpps .parameter .payment_order_segment )
1340+ vpps = self .bpd .find_segment_first ('HIVPPS' )
1341+ if not vpps :
1342+ return
1343+
1344+ needed = str (seg .header .type ) in list (vpps .parameter .payment_order_segment )
13111345
13121346 if not needed :
13131347 return
13141348
1315- bank_supported = str (self . vpps .parameter .supported_report_formats )
1349+ bank_supported = str (vpps .parameter .supported_report_formats )
13161350
13171351 if "sepade.pain.002.001.10.xsd" != bank_supported :
13181352 logger .warning ("No common supported SEPA version. Defaulting to what bank supports and hoping for the best: %s." , bank_supported )
@@ -1357,71 +1391,45 @@ def _send_with_possible_retry(self, dialog, command_seg, resume_func):
13571391 return resume_func (command_seg , response )
13581392
13591393 def _send_pay_with_possible_retry (self , dialog , command_seg , resume_func ):
1360- """This adds VoP under the assumption that TAN will be sent,
1361- There is no VoP flow without sending any authentication that I could distinguish.
1362-
1363- There are really 2 VoP flows: with a full match and otherwise. All of them return the HIVPP response in NeedTANResponse.
1364-
1365- On a full match, it's only needed to copy the vop_id alongside the TAN response.
1394+ """
1395+ This adds VoP under the assumption that TAN will be sent,
1396+ There appears to be no VoP flow without sending any authentication.
13661397
1367- In all other cases, the application should ask the user for confirmation based on HIVPP data in resp.vop_result.
1398+ There are really 2 VoP flows: with a full match and otherwise.
1399+ The second flow returns a NeedVOPResponse as intended by the specification flowcharts.
1400+ In this case cases, the application should ask the user for confirmation based on HIVPP data in resp.vop_result.
13681401
13691402 The kind of response is in resp.vop_result.single_vop_result.result:
13701403 - 'RCVC' - full match
13711404 - 'RVMC' - partial match, extra info in single_vop_result.close_match_name and .other_identification.
13721405 - 'RVNM' - no match, no extra info seen
13731406 - 'RVNA' - check not available, reason in single_vop_result.na_reason
13741407 - 'PDNG' - pending, seems related to something not implemented right now.
1375-
1376- Simple untested example.
1377-
1378- ```
1379- def process_tan(client, challenge, prompt):
1380- if challenge.vop_result and not challenge.vop_result.vop_single_result.result == 'RCVC':
1381- input("WARNING!!! Recipient name don't match:", challenge.vop_result.single_vop_result.close_match_name)
1382- print("A TAN is required:", challenge.challenge)
1383- hitan6 = challenge.tan_request
1384- ... get TAN from user
1385- return client.send_tan(challenge, tan)
1386- ```
1387-
1388- This gives the user a chance to slam ctrl+C before adding TAN.
1389-
1390- Internally, the library always sends a positive confirmation of transaction because the user only really acknowledges it by sending the TAN.
1391- Even if the legal liability moves on receiving the accepting message, there's no liability to be had before TAN goes through. Amirite?
13921408 """
13931409 vop_seg = []
13941410 vop_standard = self ._find_vop_format_for_segment (command_seg )
13951411 if vop_standard :
13961412 from .segments .auth import HKVPP1
13971413 vop_seg = [HKVPP1 (supported_reports = PSRD1 (psrd = [vop_standard ]))]
1414+
13981415 with dialog :
13991416 if self ._need_twostep_tan_for_segment (command_seg ):
14001417 tan_seg = self ._get_tan_segment (command_seg , '4' )
14011418 segments = vop_seg + [command_seg , tan_seg ]
14021419
14031420 response = dialog .send (* segments )
14041421
1405- hivpp = response .find_segment_first (HIVPP1 )
1406-
1407- if not hivpp :
1408- raise Exception ("Mising VoP reponse" )
1409- vop_result = hivpp .vop_single_result
1410- print (hivpp )
1411- if vop_result .result == 'RVNA' :
1412- print (vop_result .na_reason )
1413- vop_seg = [HKVPA1 (vop_id = hivpp .vop_id )]
1414- segments = vop_seg + [command_seg , tan_seg ]
1415- response = dialog .send (* segments )
1416- elif vop_result .result == 'RVNM' :
1417- vop_seg = [HKVPA1 (vop_id = hivpp .vop_id )]
1418- segments = vop_seg + [command_seg , tan_seg ]
1419- response = dialog .send (* segments )
1420- elif vop_result .result == 'RVMC' :
1421- vop_seg = [HKVPA1 (vop_id = hivpp .vop_id )]
1422- segments = vop_seg + [command_seg , tan_seg ]
1423- response = dialog .send (* segments )
1424- print (vop_result .result )
1422+ if vop_standard :
1423+ hivpp = response .find_segment_first (HIVPP1 , throw = True )
1424+
1425+ vop_result = hivpp .vop_single_result
1426+ if vop_result .result in ('RVNA' , 'RVNM' , 'RVMC' ): # Not Applicable, No Match, Close Match
1427+ return NeedVOPResponse (
1428+ vop_result = hivpp ,
1429+ command_seg = command_seg ,
1430+ resume_method = resume_func ,
1431+ )
1432+
14251433 for resp in response .responses (tan_seg ):
14261434 if resp .code in ('0030' , '3955' ):
14271435 return NeedTANResponse (
@@ -1445,6 +1453,33 @@ def is_challenge_structured(self):
14451453 return param .challenge_structured
14461454 return False
14471455
1456+ def approve_vop_response (self , challenge : NeedVOPResponse ):
1457+ """
1458+ Approves an operation that had a non-match VoP (verification of payee) response.
1459+
1460+ :param challenge: NeedVOPResponse to respond to
1461+ :return: New response after sending VOP response
1462+ """
1463+ with self ._get_dialog () as dialog :
1464+ vop_seg = [HKVPA1 (vop_id = challenge .vop_result .vop_id )]
1465+ tan_seg = self ._get_tan_segment (challenge .command_seg , '4' )
1466+ segments = vop_seg + [challenge .command_seg , tan_seg ]
1467+ response = dialog .send (* segments )
1468+
1469+ for resp in response .responses (tan_seg ):
1470+ if resp .code in ('0030' , '3955' ):
1471+ return NeedTANResponse (
1472+ challenge .command_seg ,
1473+ response .find_segment_first ('HITAN' ),
1474+ challenge .resume_method ,
1475+ self .is_challenge_structured (),
1476+ resp .code == '3955' ,
1477+ challenge .vop_result ,
1478+ )
1479+
1480+ resume_func = getattr (self , challenge .resume_method )
1481+ return resume_func (challenge .command_seg , response )
1482+
14481483 def send_tan (self , challenge : NeedTANResponse , tan : str ):
14491484 """
14501485 Sends a TAN to confirm a pending operation.
@@ -1457,7 +1492,6 @@ def send_tan(self, challenge: NeedTANResponse, tan: str):
14571492 :param tan: TAN value
14581493 :return: New response after sending TAN
14591494 """
1460-
14611495 with self ._get_dialog () as dialog :
14621496 if challenge .decoupled :
14631497 tan_seg = self ._get_tan_segment (challenge .command_seg , 'S' , challenge .tan_request )
@@ -1466,7 +1500,6 @@ def send_tan(self, challenge: NeedTANResponse, tan: str):
14661500 self ._pending_tan = tan
14671501
14681502 vop_seg = []
1469- print (challenge .vop_result )
14701503 if challenge .vop_result and challenge .vop_result .vop_single_result .result == 'RCVC' :
14711504 vop_seg = [HKVPA1 (vop_id = challenge .vop_result .vop_id )]
14721505 segments = vop_seg + [tan_seg ]
0 commit comments