Projet

Général

Profil

0003-add-a-method-to-guess-transaction_id-and-backend-fro.patch

Benjamin Dauvergne, 28 juin 2022 17:06

Télécharger (15,5 ko)

Voir les différences:

Subject: [PATCH 3/4] add a method to guess transaction_id and backend from an
 HTTP response (#32224)

 eopayment/__init__.py    |  27 +++++++++
 eopayment/common.py      |   4 ++
 eopayment/mollie.py      |  14 +++++
 eopayment/ogone.py       |  19 ++++++
 eopayment/paybox.py      |  14 +++++
 eopayment/payfip_ws.py   |  14 +++++
 eopayment/sips2.py       |  20 +++++-
 eopayment/systempayv2.py |  21 +++++++
 eopayment/tipi.py        |  14 +++++
 tests/test_misc.py       | 128 +++++++++++++++++++++++++++++++++++++++
 10 files changed, 274 insertions(+), 1 deletion(-)
eopayment/__init__.py
33 33
    RECEIVED,
34 34
    URL,
35 35
    WAITING,
36
    BackendNotFound,
36 37
    PaymentException,
37 38
    ResponseError,
38 39
    force_text,
......
283 284

  
284 285
    def get_maximal_amount(self):
285 286
        return getattr(self.backend, 'maximal_amount', None)
287

  
288
    @property
289
    def has_guess(self):
290
        return hasattr(self.backend.__class__, 'guess')
291

  
292
    @classmethod
293
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
294
        '''Try to guess the type of bakcned and the transaction_id given part of an HTTP response.
295
        method CAN be GET or POST.
296
        query_string is the URL encoded query-string as-is.
297
        body is the bytes content of the response.
298
        headers can eventually give access to the response headers.
299
        backends is to limit the type of backends if the possible backends are known.
300
        '''
301

  
302
        for kind, backend in get_backends().items():
303
            if not hasattr(backend, 'guess'):
304
                continue
305
            if backends and kind not in backends:
306
                continue
307
            transaction_id = backend.guess(
308
                method=method, query_string=query_string, body=body, headers=headers
309
            )
310
            if transaction_id:
311
                return kind, transaction_id
312
        raise BackendNotFound
eopayment/common.py
89 89
    pass
90 90

  
91 91

  
92
class BackendNotFound(PaymentException):
93
    pass
94

  
95

  
92 96
class PaymentResponse:
93 97
    """Holds a generic view on the result of payment transaction response.
94 98

  
eopayment/mollie.py
162 162
                '%s error on endpoint "%s": %s "%s"' % (method, endpoint, e, result.get('detail', result))
163 163
            )
164 164
        return result
165

  
166
    @classmethod
167
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
168
        for content in [query_string, body]:
169
            if isinstance(content, bytes):
170
                try:
171
                    content = content.decode()
172
                except UnicodeDecodeError:
173
                    pass
174
            if isinstance(content, str):
175
                fields = parse_qs(content)
176
                if set(fields) == {'id'}:
177
                    return fields['id'][0]
178
        return None
eopayment/ogone.py
645 645
            order_id=complus or orderid,
646 646
            transaction_id=transaction_id,
647 647
        )
648

  
649
    @classmethod
650
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
651
        for content in [query_string, body]:
652
            if isinstance(content, bytes):
653
                try:
654
                    content = content.decode()
655
                except UnicodeDecodeError:
656
                    pass
657
            if isinstance(content, str):
658
                fields = {key.upper(): values for key, values in urlparse.parse_qs(content).items()}
659
                if not set(fields) >= {'ORDERID', 'PAYID', 'STATUS', 'NCERROR'}:
660
                    continue
661
                orderid = fields.get('ORDERID')
662
                complus = fields.get('COMPLUS')
663
                if complus:
664
                    return complus[0]
665
                return orderid[0]
666
        return None
eopayment/paybox.py
455 455

  
456 456
    def cancel(self, amount, bank_data, **kwargs):
457 457
        return self.perform(amount, bank_data, PAYBOX_DIRECT_CANCEL_OPERATION)
458

  
459
    @classmethod
460
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
461
        for content in [query_string, body]:
462
            if isinstance(content, bytes):
463
                try:
464
                    content = content.decode()
465
                except UnicodeDecodeError:
466
                    pass
467
            if isinstance(content, str):
468
                fields = urlparse.parse_qs(content)
469
                if 'erreur' in fields and 'reference' in fields:
470
                    return fields['reference'][0]
471
        return None
eopayment/payfip_ws.py
374 374
            test=response.saisie == 'T',
375 375
        )
376 376

  
377
    @classmethod
378
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
379
        for content in [query_string, body]:
380
            if isinstance(content, bytes):
381
                try:
382
                    content = content.decode()
383
                except UnicodeDecodeError:
384
                    pass
385
            if isinstance(content, str):
386
                fields = parse_qs(content)
387
                if set(fields) == {'idOp'}:
388
                    return fields['idOp'][0]
389
        return None
390

  
377 391

  
378 392
if __name__ == '__main__':
379 393
    import click
eopayment/sips2.py
256 256
        self.logger.debug('emitting request %r', data)
257 257
        return transactionReference, FORM, form
258 258

  
259
    def decode_data(self, data):
259
    @classmethod
260
    def decode_data(cls, data):
260 261
        data = data.split('|')
261 262
        data = [map(force_text, p.split('=', 1)) for p in data]
262 263
        return collections.OrderedDict(data)
......
372 373
        self.logger.debug('received %r', response.content)
373 374
        response.raise_for_status()
374 375
        return response.json()
376

  
377
    @classmethod
378
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
379
        for content in [query_string, body]:
380
            if isinstance(content, bytes):
381
                try:
382
                    content = content.decode()
383
                except UnicodeDecodeError:
384
                    pass
385
            if isinstance(content, str):
386
                fields = urlparse.parse_qs(content)
387
                if not set(fields) >= {'Data', 'Seal', 'InterfaceVersion'}:
388
                    continue
389
                data = self.decode_data(fields['Data'][0])
390
                if 'transactionReference' in data:
391
                    return data['transactionReference']
392
        return None
eopayment/systempayv2.py
620 620
        sign = sign_method(secret, signed_data)
621 621
        self.logger.debug('signature «%s»', sign)
622 622
        return force_text(sign)
623

  
624
    @classmethod
625
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
626
        for content in [query_string, body]:
627
            if isinstance(content, bytes):
628
                try:
629
                    content = content.decode()
630
                except UnicodeDecodeError:
631
                    pass
632
            if isinstance(content, str):
633
                fields = urlparse.parse_qs(content)
634
                if not set(fields) >= {SIGNATURE, VADS_CTX_MODE, VADS_AUTH_RESULT}:
635
                    continue
636
                vads_eopayment_trans_id = fields.get(VADS_EOPAYMENT_TRANS_ID)
637
                vads_trans_date = fields.get(VADS_TRANS_DATE)
638
                vads_trans_id = fields.get(VADS_TRANS_ID)
639
                if vads_eopayment_trans_id:
640
                    return vads_eopayment_trans_id[0]
641
                elif vads_trans_date and vads_trans_id:
642
                    return vads_trans_date[0] + '_' + vads_trans_id[0]
643
        return None
eopayment/tipi.py
202 202
            transaction_id=refdet,
203 203
            test=test,
204 204
        )
205

  
206
    @classmethod
207
    def guess(self, *, method=None, query_string=None, body=None, headers=None, backends=(), **kwargs):
208
        for content in [query_string, body]:
209
            if isinstance(content, bytes):
210
                try:
211
                    content = content.decode()
212
                except UnicodeDecodeError:
213
                    pass
214
            if isinstance(content, str):
215
                fields = parse_qs(content)
216
                if 'refdet' in fields and 'resultrans' in fields:
217
                    return fields['refdet'][0]
218
        return None
tests/test_misc.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import pytest
17 18

  
18 19
import eopayment
19 20

  
20 21

  
21 22
def test_get_backends():
22 23
    assert len(eopayment.get_backends()) > 1
24

  
25

  
26
GUESS_TEST_VECTORS = [
27
    {
28
        'name': 'tipi',
29
        'kwargs': {
30
            'query_string': 'objet=tout+a+fait&montant=12312&saisie=T&mel=info%40entrouvert.com'
31
            '&numcli=12345&exer=9999&refdet=999900000000999999&resultrans=P',
32
        },
33
        'result': ['tipi', '999900000000999999'],
34
    },
35
    {
36
        'name': 'payfip_ws',
37
        'kwargs': {
38
            'query_string': 'idOp=1234',
39
        },
40
        'result': ['payfip_ws', '1234'],
41
    },
42
    {
43
        'name': 'systempayv2-old-transaction-id',
44
        'kwargs': {
45
            'query_string': 'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf'
46
            '&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB'
47
            '&vads_result=00'
48
            '&vads_card_number=497010XXXXXX0000'
49
            '&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53'
50
            '&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042'
51
            '&vads_site_id=70168983&vads_trans_date=20161013101355'
52
            '&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a'
53
            '&vads_effective_creation_date=20200330162530'
54
            '&signature=c17fab393f94dc027dc029510c85d5fc46c4710f',
55
        },
56
        'result': ['systempayv2', '20161013101355_226787'],
57
    },
58
    {
59
        'name': 'systempayv2-eo-trans-id',
60
        'kwargs': {
61
            'query_string': 'vads_amount=1042&vads_auth_mode=FULL&vads_auth_number=3feadf'
62
            '&vads_auth_result=00&vads_capture_delay=0&vads_card_brand=CB'
63
            '&vads_result=00'
64
            '&vads_card_number=497010XXXXXX0000'
65
            '&vads_payment_certificate=582ba2b725057618706d7a06e9e59acdbf69ff53'
66
            '&vads_ctx_mode=TEST&vads_currency=978&vads_effective_amount=1042'
67
            '&vads_site_id=70168983&vads_trans_date=20161013101355'
68
            '&vads_trans_id=226787&vads_trans_uuid=4b5053b3b1fe4b02a07753e7a'
69
            '&vads_effective_creation_date=20200330162530'
70
            '&signature=c17fab393f94dc027dc029510c85d5fc46c4710f'
71
            '&vads_ext_info_eopayment_trans_id=123456',
72
        },
73
        'result': ['systempayv2', '123456'],
74
    },
75
    {
76
        'name': 'paybox',
77
        'kwargs': {
78
            'query_string': 'montant=4242&reference=abcdef&code_autorisation=A'
79
            '&erreur=00000&date_transaction=20200101&heure_transaction=01%3A01%3A01',
80
        },
81
        'result': ['paybox', 'abcdef'],
82
    },
83
    {
84
        'name': 'ogone-no-complus',
85
        'kwargs': {
86
            'query_string': 'orderid=myorder&status=9&payid=3011229363&cn=Us%C3%A9r&ncerror=0'
87
            '&trxdate=10%2F24%2F16&acceptance=test123&currency=eur&amount=7.5',
88
        },
89
        'result': ['ogone', 'myorder'],
90
    },
91
    {
92
        'name': 'ogone-with-complus',
93
        'kwargs': {
94
            'query_string': 'complus=neworder&orderid=myorder&status=9&payid=3011229363&cn=Us%C3%A9r'
95
            '&ncerror=0&trxdate=10%2F24%2F16&acceptance=test123&currency=eur&amount=7.5',
96
        },
97
        'result': ['ogone', 'neworder'],
98
    },
99
    {
100
        'name': 'mollie',
101
        'kwargs': {
102
            'body': b'id=tr_7UhSN1zuXS',
103
        },
104
        'result': ['mollie', 'tr_7UhSN1zuXS'],
105
    },
106
    {
107
        'name': 'sips2',
108
        'kwargs': {
109
            'body': (
110
                b'Data=captureDay%3D0%7CcaptureMode%3DAUTHOR_CAPTURE%7CcurrencyCode%3D978%7CmerchantId%3D002001000000001%7CorderChannel%3D'
111
                b'INTERNET%7CresponseCode%3D00%7CtransactionDateTime%3D2016-02-01T17%3A44%3A20%2B01%3A00%7C'
112
                b'transactionReference%3D668930%7CkeyVersion%3D1%7CacquirerResponseCode%3D00%7Camou'
113
                b'nt%3D1200%7CauthorisationId%3D12345%7CcardCSCResultCode%3D4E%7CpanExpiryDate%3D201605%7Cpay'
114
                b'mentMeanBrand%3DMASTERCARD%7CpaymentMeanType%3DCARD%7CcustomerIpAddress%3D82.244.203.243%7CmaskedPan'
115
                b'%3D5100%23%23%23%23%23%23%23%23%23%23%23%2300%7CorderId%3Dd4903de7027f4d56ac01634fd7ab9526%7CholderAuthentRelegation'
116
                b'%3DN%7CholderAuthentStatus%3D3D_ERROR%7CtransactionOrigin%3DINTERNET%7CpaymentPattern%3D'
117
                b'ONE_SHOT&Seal=6ca3247765a19b45d25ad54ef4076483e7d55583166bd5ac9c64357aac097602&InterfaceVersion=HP_2.0&Encode='
118
            ),
119
        },
120
        'result': ['sips2', '668930'],
121
    },
122
    {
123
        'name': 'notfound',
124
        'kwargs': {},
125
        'exception': eopayment.BackendNotFound,
126
    },
127
    {
128
        'name': 'notfound-2',
129
        'kwargs': {'query_string': None, 'body': [12323], 'headers': {b'1': '2'}},
130
        'exception': eopayment.BackendNotFound,
131
    },
132
    {
133
        'name': 'backends-limitation',
134
        'kwargs': {
135
            'body': b'id=tr_7UhSN1zuXS',
136
            'backends': ['payfips_ws'],
137
        },
138
        'exception': eopayment.BackendNotFound,
139
    },
140
]
141

  
142

  
143
@pytest.mark.parametrize('test_vector', GUESS_TEST_VECTORS, ids=lambda tv: tv['name'])
144
def test_guess(test_vector):
145
    kwargs, result, exception = test_vector['kwargs'], test_vector.get('result'), test_vector.get('exception')
146
    if exception is not None:
147
        with pytest.raises(exception):
148
            eopayment.Payment.guess(**kwargs)
149
    else:
150
        assert list(eopayment.Payment.guess(**kwargs)) == result
23
-