1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
|
# -*- coding: utf-8 -*-
import json
import logging
import requests
from uuid import uuid4
from odoo import _
from odoo.exceptions import UserError
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
_logger = logging.getLogger(__name__)
class AuthorizeAPI():
"""Authorize.net Gateway API integration.
This class allows contacting the Authorize.net API with simple operation
requests. It implements a *very limited* subset of the complete API
(http://developer.authorize.net/api/reference); namely:
- Customer Profile/Payment Profile creation
- Transaction authorization/capture/voiding
"""
AUTH_ERROR_STATUS = 3
def __init__(self, acquirer):
"""Initiate the environment with the acquirer data.
:param record acquirer: payment.acquirer account that will be contacted
"""
if acquirer.state == 'test':
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
else:
self.url = 'https://api.authorize.net/xml/v1/request.api'
self.state = acquirer.state
self.name = acquirer.authorize_login
self.transaction_key = acquirer.authorize_transaction_key
def _authorize_request(self, data):
_logger.info('_authorize_request: Sending values to URL %s, values:\n%s', self.url, data)
resp = requests.post(self.url, json.dumps(data))
resp.raise_for_status()
resp = json.loads(resp.content)
_logger.info("_authorize_request: Received response:\n%s", resp)
messages = resp.get('messages')
if messages and messages.get('resultCode') == 'Error':
return {
'err_code': messages.get('message')[0].get('code'),
'err_msg': messages.get('message')[0].get('text')
}
return resp
# Customer profiles
def create_customer_profile(self, partner, opaqueData):
"""Create a payment and customer profile in the Authorize.net backend.
Creates a customer profile for the partner/credit card combination and links
a corresponding payment profile to it. Note that a single partner in the Odoo
database can have multiple customer profiles in Authorize.net (i.e. a customer
profile is created for every res.partner/payment.token couple).
:param record partner: the res.partner record of the customer
:param str cardnumber: cardnumber in string format (numbers only, no separator)
:param str expiration_date: expiration date in 'YYYY-MM' string format
:param str card_code: three- or four-digit verification number
:return: a dict containing the profile_id and payment_profile_id of the
newly created customer profile and payment profile
:rtype: dict
"""
values = {
'createCustomerProfileRequest': {
'merchantAuthentication': {
'name': self.name,
'transactionKey': self.transaction_key
},
'profile': {
'description': ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20],
'email': partner.email or '',
'paymentProfiles': {
'customerType': 'business' if partner.is_company else 'individual',
'billTo': {
'firstName': '' if partner.is_company else _partner_split_name(partner.name)[0],
'lastName': _partner_split_name(partner.name)[1],
'address': (partner.street or '' + (partner.street2 if partner.street2 else '')) or None,
'city': partner.city,
'state': partner.state_id.name or None,
'zip': partner.zip or '',
'country': partner.country_id.name or None,
'phoneNumber': partner.phone or '',
},
'payment': {
'opaqueData': {
'dataDescriptor': opaqueData.get('dataDescriptor'),
'dataValue': opaqueData.get('dataValue')
}
}
}
},
'validationMode': 'liveMode' if self.state == 'enabled' else 'testMode'
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
raise UserError(_(
"Authorize.net Error:\nCode: %s\nMessage: %s",
response.get('err_code'), response.get('err_msg'),
))
return {
'profile_id': response.get('customerProfileId'),
'payment_profile_id': response.get('customerPaymentProfileIdList')[0]
}
def create_customer_profile_from_tx(self, partner, transaction_id):
"""Create an Auth.net payment/customer profile from an existing transaction.
Creates a customer profile for the partner/credit card combination and links
a corresponding payment profile to it. Note that a single partner in the Odoo
database can have multiple customer profiles in Authorize.net (i.e. a customer
profile is created for every res.partner/payment.token couple).
Note that this function makes 2 calls to the authorize api, since we need to
obtain a partial cardnumber to generate a meaningful payment.token name.
:param record partner: the res.partner record of the customer
:param str transaction_id: id of the authorized transaction in the
Authorize.net backend
:return: a dict containing the profile_id and payment_profile_id of the
newly created customer profile and payment profile as well as the
last digits of the card number
:rtype: dict
"""
values = {
'createCustomerProfileFromTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transId': transaction_id,
'customer': {
'merchantCustomerId': ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20],
'email': partner.email or ''
}
}
}
response = self._authorize_request(values)
if not response.get('customerProfileId'):
_logger.warning(
'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s'
% (transaction_id, partner)
)
return False
res = {
'profile_id': response.get('customerProfileId'),
'payment_profile_id': response.get('customerPaymentProfileIdList')[0]
}
values = {
'getCustomerPaymentProfileRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'customerProfileId': res['profile_id'],
'customerPaymentProfileId': res['payment_profile_id'],
}
}
response = self._authorize_request(values)
res['name'] = response.get('paymentProfile', {}).get('payment', {}).get('creditCard', {}).get('cardNumber')
return res
# Transaction management
def auth_and_capture(self, token, amount, reference):
"""Authorize and capture a payment for the given amount.
Authorize and immediately capture a payment for the given payment.token
record for the specified amount with reference as communication.
:param record token: the payment.token record that must be charged
:param str amount: transaction amount (up to 15 digits with decimal point)
:param str reference: used as "invoiceNumber" in the Authorize.net backend
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'authCaptureTransaction',
'amount': str(amount),
'profile': {
'customerProfileId': token.authorize_profile,
'paymentProfile': {
'paymentProfileId': token.acquirer_ref,
}
},
'order': {
'invoiceNumber': reference[:20],
'description': reference[:255],
}
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
result = {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'auth_capture'
}
errors = response.get('transactionResponse', {}).get('errors')
if errors:
result['x_response_reason_text'] = '\n'.join([e.get('errorText') for e in errors])
return result
def authorize(self, token, amount, reference):
"""Authorize a payment for the given amount.
Authorize (without capture) a payment for the given payment.token
record for the specified amount with reference as communication.
:param record token: the payment.token record that must be charged
:param str amount: transaction amount (up to 15 digits with decimal point)
:param str reference: used as "invoiceNumber" in the Authorize.net backend
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'authOnlyTransaction',
'amount': str(amount),
'profile': {
'customerProfileId': token.authorize_profile,
'paymentProfile': {
'paymentProfileId': token.acquirer_ref,
}
},
'order': {
'invoiceNumber': reference[:20],
'description': reference[:255],
}
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'auth_only'
}
def capture(self, transaction_id, amount):
"""Capture a previously authorized payment for the given amount.
Capture a previsouly authorized payment. Note that the amount is required
even though we do not support partial capture.
:param str transaction_id: id of the authorized transaction in the
Authorize.net backend
:param str amount: transaction amount (up to 15 digits with decimal point)
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'priorAuthCaptureTransaction',
'amount': str(amount),
'refTransId': transaction_id,
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'prior_auth_capture'
}
def void(self, transaction_id):
"""Void a previously authorized payment.
:param str transaction_id: the id of the authorized transaction in the
Authorize.net backend
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'voidTransaction',
'refTransId': transaction_id
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'void'
}
# Test
def test_authenticate(self):
"""Test Authorize.net communication with a simple credentials check.
:return: True if authentication was successful, else False (or throws an error)
:rtype: bool
"""
values = {
'authenticateTestRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return False
return True
# Client Key
def get_client_secret(self):
""" Create a client secret that will be needed for the AcceptJS integration. """
values = {
"getMerchantDetailsRequest": {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key,
}
}
}
response = self._authorize_request(values)
client_secret = response.get('publicClientKey')
return client_secret
|