You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import datetime
  2. from xml.dom.minidom import Document, parseString
  3. import httplib
  4. import urllib
  5. from django.conf import settings
  6. from django.db import transaction
  7. from django.utils.translation import ugettext_lazy as _
  8. from django.core.mail import mail_admins
  9. from oscar.core.loading import import_module
  10. import_module('payment.datacash.models', ['OrderTransaction'], locals())
  11. import_module('payment.exceptions', ['TransactionDeclined', 'GatewayError',
  12. 'InvalidGatewayRequestError'], locals())
  13. # Status codes
  14. ACCEPTED, DECLINED, INVALID_CREDENTIALS = '1', '7', '10'
  15. class Gateway(object):
  16. def __init__(self, client, password, host, cv2avs=False):
  17. self._client = client
  18. self._password = password
  19. self._host = host
  20. # Fraud settings
  21. self._cv2avs = cv2avs
  22. def do_request(self, request_xml):
  23. # Need to fill in HTTP request here
  24. conn = httplib.HTTPSConnection(self._host, 443, timeout=30)
  25. headers = {"Content-type": "application/xml",
  26. "Accept": ""}
  27. conn.request("POST", "/Transaction", request_xml, headers)
  28. response = conn.getresponse()
  29. response_xml = response.read()
  30. if response.status != httplib.OK:
  31. raise GatewayError("Unable to communicate with payment gateway (code: %s, response: %s)" % (response.status, response_xml))
  32. conn.close()
  33. # Save response XML
  34. self._last_response_xml = response_xml
  35. return response_xml
  36. def _build_request_xml(self, method_name, **kwargs):
  37. """
  38. Builds the XML for a 'initial' transaction
  39. """
  40. doc = Document()
  41. req = self._create_element(doc, doc, 'Request')
  42. # Authentication
  43. auth = self._create_element(doc, req, 'Authentication')
  44. self._create_element(doc, auth, 'client', self._client)
  45. self._create_element(doc, auth, 'password', self._password)
  46. # Transaction
  47. txn = self._create_element(doc, req, 'Transaction')
  48. # CardTxn
  49. if 'card_number' in kwargs:
  50. card_txn = self._create_element(doc, txn, 'CardTxn')
  51. self._create_element(doc, card_txn, 'method', method_name)
  52. card = self._create_element(doc, card_txn, 'Card')
  53. self._create_element(doc, card, 'pan', kwargs['card_number'])
  54. self._create_element(doc, card, 'expirydate', kwargs['expiry_date'])
  55. if 'start_date' in kwargs:
  56. self._create_element(doc, card, 'startdate', kwargs['start_date'])
  57. if 'issue_number' in kwargs:
  58. self._create_element(doc, card, 'issuenumber', kwargs['issue_number'])
  59. if 'auth_code' in kwargs:
  60. self._create_element(doc, card, 'authcode', kwargs['auth_code'])
  61. if self._cv2avs:
  62. self._add_cv2avs_elements(doc, card, kwargs)
  63. # HistoricTxn
  64. if 'txn_reference' in kwargs:
  65. historic_txn = self._create_element(doc, txn, 'HistoricTxn')
  66. self._create_element(doc, historic_txn, 'reference', kwargs['txn_reference'])
  67. self._create_element(doc, historic_txn, 'method', method_name)
  68. if 'auth_code' in kwargs:
  69. self._create_element(doc, historic_txn, 'authcode', kwargs['auth_code'])
  70. # TxnDetails
  71. if 'amount' in kwargs:
  72. txn_details = self._create_element(doc, txn, 'TxnDetails')
  73. if 'merchant_reference' in kwargs:
  74. self._create_element(doc, txn_details, 'merchantreference', kwargs['merchant_reference'])
  75. self._create_element(doc, txn_details, 'amount', str(kwargs['amount']), {'currency': kwargs['currency']})
  76. # Save XML for later retrieval
  77. self._last_request_xml = doc.toxml()
  78. return self.do_request(doc.toxml())
  79. def _add_cv2avs_elements(self, doc, card, kwargs):
  80. cv2avs = self._create_element(doc, card, 'Cv2Avs')
  81. if 'ccv' in kwargs:
  82. self._create_element(doc, cv2avs, 'cv2', kwargs['ccv'])
  83. def _create_element(self, doc, parent, tag, value=None, attributes=None):
  84. """
  85. Creates an XML element
  86. """
  87. ele = doc.createElement(tag)
  88. parent.appendChild(ele)
  89. if value:
  90. text = doc.createTextNode(value)
  91. ele.appendChild(text)
  92. if attributes:
  93. [ele.setAttribute(k, v) for k,v in attributes.items()]
  94. return ele
  95. def _get_element_text(self, doc, tag):
  96. try:
  97. ele = doc.getElementsByTagName(tag)[0]
  98. except IndexError:
  99. return None
  100. return ele.firstChild.data
  101. def _build_response_dict(self, response_xml, extra_elements=None):
  102. doc = parseString(response_xml)
  103. response = {'status': int(self._get_element_text(doc, 'status')),
  104. 'datacash_reference': self._get_element_text(doc, 'datacash_reference'),
  105. 'merchant_reference': self._get_element_text(doc, 'merchantreference'),
  106. 'reason': self._get_element_text(doc, 'reason')}
  107. if extra_elements:
  108. for tag, key in extra_elements.items():
  109. response[key] = self._get_element_text(doc, tag)
  110. return response
  111. # API
  112. def auth(self, **kwargs):
  113. """
  114. Performs an 'auth' request, which is to debit the money immediately
  115. as a one-off transaction.
  116. Note that currency should be ISO 4217 Alphabetic format.
  117. """
  118. self._check_kwargs(kwargs, ['amount', 'currency', 'card_number', 'expiry_date', 'merchant_reference'])
  119. response_xml = self._build_request_xml('auth', **kwargs)
  120. return self._build_response_dict(response_xml, {'authcode': 'auth_code'})
  121. def pre(self, **kwargs):
  122. """
  123. Performs an 'pre' request, which is to ring-fence the requested money
  124. so it can be fulfilled at a later time.
  125. """
  126. self._check_kwargs(kwargs, ['amount', 'currency', 'card_number', 'expiry_date', 'merchant_reference'])
  127. response_xml = self._build_request_xml('pre', **kwargs)
  128. return self._build_response_dict(response_xml, {'authcode': 'auth_code'})
  129. def refund(self, **kwargs):
  130. self._check_kwargs(kwargs, ['amount', 'currency', 'card_number', 'expiry_date', 'merchant_reference'])
  131. response_xml = self._build_request_xml('refund', **kwargs)
  132. return self._build_response_dict(response_xml, {'authcode': 'auth_code'})
  133. def erp(self, **kwargs):
  134. self._check_kwargs(kwargs, ['amount', 'currency', 'card_number', 'expiry_date', 'merchant_reference'])
  135. response_xml = self._build_request_xml('erp', **kwargs)
  136. return self._build_response_dict(response_xml, {'authcode': 'auth_code'})
  137. # "Historic" transaction types
  138. def cancel(self, txn_reference):
  139. response_xml = self._build_request_xml('cancel', txn_reference=txn_reference)
  140. return self._build_response_dict(response_xml)
  141. def fulfil(self, **kwargs):
  142. self._check_kwargs(kwargs, ['amount', 'currency', 'txn_reference', 'auth_code'])
  143. response_xml = self._build_request_xml('fulfil', **kwargs)
  144. return self._build_response_dict(response_xml)
  145. def txn_refund(self, **kwargs):
  146. self._check_kwargs(kwargs, ['amount', 'currency', 'txn_reference'])
  147. response_xml = self._build_request_xml('txn_refund', **kwargs)
  148. return self._build_response_dict(response_xml)
  149. def last_request_xml(self):
  150. return self._last_request_xml
  151. def last_response_xml(self):
  152. return self._last_response_xml
  153. def _check_kwargs(self, kwargs, required_keys):
  154. for key in required_keys:
  155. if key not in kwargs:
  156. raise RuntimeError('You must provide a "%s" argument' % key)
  157. class Facade(object):
  158. """
  159. Responsible for dealing with oscar objects
  160. """
  161. def __init__(self):
  162. self.gateway = Gateway(settings.DATACASH_CLIENT, settings.DATACASH_PASSWORD, settings.DATACASH_HOST)
  163. def debit(self, order_number, amount, bankcard, basket, billing_address=None):
  164. with transaction.commit_on_success():
  165. response = self.gateway.auth(card_number=bankcard.card_number,
  166. expiry_date=bankcard.expiry_date,
  167. amount=amount,
  168. currency='GBP',
  169. merchant_reference=self.generate_merchant_reference(order_number),
  170. ccv=bankcard.ccv)
  171. # Create transaction model irrespective of whether transaction was successful or not
  172. txn = OrderTransaction.objects.create(order_number=order_number,
  173. basket=basket,
  174. method='auth',
  175. datacash_ref=response['datacash_reference'],
  176. merchant_ref=response['merchant_reference'],
  177. amount=amount,
  178. auth_code=response['auth_code'],
  179. status=int(response['status']),
  180. reason=response['reason'],
  181. request_xml=self.gateway.last_request_xml(),
  182. response_xml=self.gateway.last_response_xml())
  183. # Test if response is successful
  184. if response['status'] == INVALID_CREDENTIALS:
  185. # This needs to notify the administrators straight away
  186. import pprint
  187. msg = "Order #%s:\n%s" % (order_number, pprint.pprint(response))
  188. mail_admins("Datacash credentials are not valid", msg)
  189. raise InvalidGatewayRequestError("Unable to communicate with payment gateway, please try again later")
  190. if response['status'] == DECLINED:
  191. raise TransactionDeclined("Your bank declined this transaction, please check your details and try again")
  192. return response['datacash_reference']
  193. def generate_merchant_reference(self, order_number):
  194. return '%s_%s' % (order_number, datetime.datetime.now().microsecond)