class Facade(object): """ A bridge between oscar's objects and the core gateway object """ def __init__(self): self.gateway = Gateway( settings.PAYMENTEXPRESS_POST_URL, settings.PAYMENTEXPRESS_USERNAME, settings.PAYMENTEXPRESS_PASSWORD, getattr(settings, 'PAYMENTEXPRESS_CURRENCY', 'AUD')) def _check_amount(self, amount): if amount == 0 or amount is None: raise UnableToTakePayment("Order amount must be non-zero") def _get_merchant_reference(self, order_number, txn_type): num_previous = OrderTransaction.objects.filter( order_number=order_number, txn_type=txn_type).count() # Get a random number to append to the end. This solves the problem # where a previous request crashed out and didn't save a model instance # Hence we can get a clash of merchant references. rand = "%04.f" % (random.random() * 10000) return u'%s_%s_%d_%s' % (order_number, txn_type.upper(), num_previous + 1, rand) def _get_friendly_decline_message(self): return ('The transaction was declined by your bank - ' + 'please check your bankcard details and try again') def _handle_response(self, txn_type, order_number, amount, response): OrderTransaction.objects.create( order_number=order_number, txn_type=txn_type, txn_ref=response['dps_txn_ref'], amount=amount, response_code=response['response_code'], response_message=response.get_message(), request_xml=response.request_xml, response_xml=response.response_xml) if response.is_successful(): return { 'txn_reference': response['dps_txn_ref'], 'partner_reference': response['dps_billing_id'], } elif response.is_declined(): raise UnableToTakePayment(self._get_friendly_decline_message()) else: raise InvalidGatewayRequestError(response.get_message()) def _format_card_date(self, str_date): # Dirty hack so that Oscar's BankcardForm doesn't need to be overridden if str_date is None: return None return str_date.replace('/', '') def authorise(self, order_number, amount, bankcard): """ Authorizes a transaction. Must be completed within 7 days using the "Complete" TxnType """ self._check_amount(amount) card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) merchant_ref = self._get_merchant_reference(order_number, AUTH) res = self.gateway.authorise(card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, amount=amount, merchant_ref=merchant_ref) return self._handle_response(AUTH, order_number, amount, res) def complete(self, order_number, amount, dps_txn_ref): """ Completes (settles) a pre-approved Auth Transaction. The DpsTxnRef value returned by the original approved Auth transaction must be supplied. """ self._check_amount(amount) merchant_ref = self._get_merchant_reference(order_number, COMPLETE) res = self.gateway.complete(amount=amount, dps_txn_ref=dps_txn_ref, merchant_ref=merchant_ref) return self._handle_response(COMPLETE, order_number, amount, res) def purchase(self, order_number, amount, billing_id=None, bankcard=None): """ Purchase - Funds are transferred immediately. """ self._check_amount(amount) res = None merchant_ref = self._get_merchant_reference(order_number, PURCHASE) if billing_id: res = self.gateway.purchase(amount=amount, dps_billing_id=billing_id, merchant_ref=merchant_ref) elif bankcard: card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) res = self.gateway.purchase(amount=amount, card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, merchant_ref=merchant_ref, enable_add_bill_card=1) else: raise ValueError("You must specify either a billing id or " + "a merchant reference") return self._handle_response(PURCHASE, order_number, amount, res) def refund(self, order_number, amount, dps_txn_ref): """ Refund - Funds transferred immediately. Must be enabled as a special option. """ self._check_amount(amount) merchant_ref = self._get_merchant_reference(order_number, REFUND) res = self.gateway.refund(amount=amount, dps_txn_ref=dps_txn_ref, merchant_ref=merchant_ref) return self._handle_response(REFUND, order_number, amount, res) def validate(self, bankcard): """ Validation Transaction. Effects a $1.00 Auth to validate card details including expiry date. Often utilised with the EnableAddBillCard property set to 1 to automatically add to Billing Database if the transaction is approved. """ amount = 1.00 card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) res = self.gateway.validate(amount=amount, card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, enable_add_bill_card=1) return self._handle_response(VALIDATE, None, amount, res)
class GatewayTests(TestCase): gateway = None def setUp(self): self.gateway = Gateway('http://localhost/', 'TangentSnowball', 's3cr3t', 'AUD') def test_raises_error_on_missing_fields(self): with self.assertRaises(ValueError): self.gateway.authorise( card_holder='Frankie', card_number='5111222233334444', amount=1.23 ) def test_does_not_raise_error_when_all_fields_set(self): r = Request('TangentSnowball', 's3cr3t', 'AUD', 'Purchase', 1.23) doc = parseString(r.request_xml) self.assertIsInstance(doc, Document) def test_authorise_fields_set(self): self.gateway.authorise( card_holder='Frankie', card_number='5111222233334444', cvc2='123', amount=1.23 ) with self.assertRaises(ValueError): self.gateway.authorise() def test_amount_is_always_required(self): r = self.gateway._get_request(AUTH, {'card_holder': 'Frankie', 'amount': 1.23 }, ['amount', ], ) self.assertIsInstance(r, Request) with self.assertRaises(ValueError): self.gateway._get_request(AUTH, {'card_holder': 'Frankie'}, [],) def test_get_message_returns_help_text(self): r = Response(SAMPLE_PURCHASE_REQUEST, SAMPLE_SUCCESSFUL_RESPONSE) self.assertTrue(r.get_message() is not None) def test_complete_requires_dps_txn_ref(self): with self.assertRaises(ValueError): self.gateway.complete() def test_amount_always_greater_than_zero(self): self.gateway.complete(dps_txn_ref="1234", amount=1.23) with self.assertRaises(ValueError): self.gateway.authorise(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", amount=0.00) self.gateway.complete(dps_txn_ref="1234", amount=0) def test_card_expiry_has_four_digits(self): self.gateway.validate(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", card_expiry="1015", amount=1.00) with self.assertRaises(ValueError): self.gateway.validate(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", card_expiry="10/15", amount=1.00) def test_currency_code_has_three_characters(self): gateway = Gateway('http://localhost/', 'TangentSnowball', 's3cr3t', 'au') with self.assertRaises(ValueError): gateway.refund(dps_txn_ref="abc", merchant_ref="123", amount=1.23)
class ApiResponseTests(MockedResponseTestCase): def setUp(self): self.gateway = Gateway( post_url='https://sec.paymentexpress.com/pxpost.aspx', username='******', password='******', currency='AUD') def test_authorise_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE ) self.assertIsInstance( self.gateway.authorise(card_holder='Frankie', card_number=CARD_VISA, cvc2='123', amount=1.23), Response ) def test_complete_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.complete(dps_txn_ref='1234', amount=1.23, ), Response) def test_purchase_with_billing_id_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE ) self.assertIsInstance( self.gateway.purchase(dps_billing_id='123', amount=1.23), Response ) def test_purchase_with_bankcard_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE ) self.assertIsInstance( self.gateway.purchase(card_holder='Frankie', card_number=CARD_VISA, card_expiry='1015', cvc2='123', merchant_ref='abc123', enable_add_bill_card=1, amount=1.23), Response ) def test_refund_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE ) self.assertIsInstance( self.gateway.refund(dps_txn_ref='1234', merchant_ref='abc123', amount=1.23), Response ) def test_validate_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE ) self.assertIsInstance( self.gateway.validate(card_holder='Frankie', card_number=CARD_VISA, cvc2='123', card_expiry='1015', amount=1.23), Response )
class GatewayTests(TestCase): gateway = None def setUp(self): self.gateway = Gateway('http://localhost/', 'TangentSnowball', 's3cr3t', 'AUD') def test_raises_error_on_missing_fields(self): with self.assertRaises(ValueError): self.gateway.authorise(card_holder='Frankie', card_number='5111222233334444', amount=1.23) def test_does_not_raise_error_when_all_fields_set(self): r = Request('TangentSnowball', 's3cr3t', 'AUD', 'Purchase', 1.23) doc = parseString(r.request_xml) self.assertIsInstance(doc, Document) def test_authorise_fields_set(self): self.gateway.authorise(card_holder='Frankie', card_number='5111222233334444', cvc2='123', amount=1.23) with self.assertRaises(ValueError): self.gateway.authorise() def test_amount_is_always_required(self): r = self.gateway._get_request( AUTH, { 'card_holder': 'Frankie', 'amount': 1.23 }, [ 'amount', ], ) self.assertIsInstance(r, Request) with self.assertRaises(ValueError): self.gateway._get_request( AUTH, {'card_holder': 'Frankie'}, [], ) def test_get_message_returns_help_text(self): r = Response(SAMPLE_PURCHASE_REQUEST, SAMPLE_SUCCESSFUL_RESPONSE) self.assertTrue(r.get_message() is not None) def test_complete_requires_dps_txn_ref(self): with self.assertRaises(ValueError): self.gateway.complete() def test_amount_always_greater_than_zero(self): self.gateway.complete(dps_txn_ref="1234", amount=1.23) with self.assertRaises(ValueError): self.gateway.authorise(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", amount=0.00) self.gateway.complete(dps_txn_ref="1234", amount=0) def test_card_expiry_has_four_digits(self): self.gateway.validate(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", card_expiry="1015", amount=1.00) with self.assertRaises(ValueError): self.gateway.validate(card_holder="Frankie", card_number=CARD_VISA, cvc2="123", card_expiry="10/15", amount=1.00) def test_currency_code_has_three_characters(self): gateway = Gateway('http://localhost/', 'TangentSnowball', 's3cr3t', 'au') with self.assertRaises(ValueError): gateway.refund(dps_txn_ref="abc", merchant_ref="123", amount=1.23)
class ApiResponseTests(MockedResponseTestCase): def setUp(self): self.gateway = Gateway( post_url='https://sec.paymentexpress.com/pxpost.aspx', username='******', password='******', currency='AUD') def test_authorise_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.authorise(card_holder='Frankie', card_number=CARD_VISA, cvc2='123', amount=1.23), Response) def test_complete_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.complete( dps_txn_ref='1234', amount=1.23, ), Response) def test_purchase_with_billing_id_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.purchase(dps_billing_id='123', amount=1.23), Response) def test_purchase_with_bankcard_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.purchase(card_holder='Frankie', card_number=CARD_VISA, card_expiry='1015', cvc2='123', merchant_ref='abc123', enable_add_bill_card=1, amount=1.23), Response) def test_refund_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.refund(dps_txn_ref='1234', merchant_ref='abc123', amount=1.23), Response) def test_validate_returns_response(self): with patch('requests.post') as post: post.return_value = self.create_mock_response( SAMPLE_SUCCESSFUL_RESPONSE) self.assertIsInstance( self.gateway.validate(card_holder='Frankie', card_number=CARD_VISA, cvc2='123', card_expiry='1015', amount=1.23), Response)
class Facade(object): """ A bridge between oscar's objects and the core gateway object """ def __init__(self): self.gateway = Gateway( settings.PAYMENTEXPRESS_POST_URL, settings.PAYMENTEXPRESS_USERNAME, settings.PAYMENTEXPRESS_PASSWORD, getattr(settings, 'PAYMENTEXPRESS_CURRENCY', 'AUD') ) def _check_amount(self, amount): if amount == 0 or amount is None: raise UnableToTakePayment("Order amount must be non-zero") def _get_merchant_reference(self, order_number, txn_type): num_previous = OrderTransaction.objects.filter( order_number=order_number, txn_type=txn_type).count() # Get a random number to append to the end. This solves the problem # where a previous request crashed out and didn't save a model instance # Hence we can get a clash of merchant references. rand = "%04.f" % (random.random() * 10000) return u'%s_%s_%d_%s' % ( order_number, txn_type.upper(), num_previous + 1, rand ) def _get_friendly_decline_message(self): return ('The transaction was declined by your bank - ' + 'please check your bankcard details and try again') def _handle_response(self, txn_type, order_number, amount, response): OrderTransaction.objects.create( order_number=order_number, txn_type=txn_type, txn_ref=response['dps_txn_ref'], amount=amount, response_code=response['response_code'], response_message=response.get_message(), request_xml=response.request_xml, response_xml=response.response_xml ) if response.is_successful(): return { 'txn_reference': response['dps_txn_ref'], 'partner_reference': response['dps_billing_id'], } elif response.is_declined(): raise UnableToTakePayment(self._get_friendly_decline_message()) else: raise InvalidGatewayRequestError(response.get_message()) def _format_card_date(self, str_date): # Dirty hack so that Oscar's BankcardForm doesn't need to be overridden if str_date is None: return None return str_date.replace('/', '') def authorise(self, order_number, amount, bankcard): """ Authorizes a transaction. Must be completed within 7 days using the "Complete" TxnType """ self._check_amount(amount) card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) merchant_ref = self._get_merchant_reference(order_number, AUTH) res = self.gateway.authorise(card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, amount=amount, merchant_ref=merchant_ref) return self._handle_response(AUTH, order_number, amount, res) def complete(self, order_number, amount, dps_txn_ref): """ Completes (settles) a pre-approved Auth Transaction. The DpsTxnRef value returned by the original approved Auth transaction must be supplied. """ self._check_amount(amount) merchant_ref = self._get_merchant_reference(order_number, COMPLETE) res = self.gateway.complete(amount=amount, dps_txn_ref=dps_txn_ref, merchant_ref=merchant_ref) return self._handle_response(COMPLETE, order_number, amount, res) def purchase(self, order_number, amount, billing_id=None, bankcard=None): """ Purchase - Funds are transferred immediately. """ self._check_amount(amount) res = None merchant_ref = self._get_merchant_reference(order_number, PURCHASE) if billing_id: res = self.gateway.purchase(amount=amount, dps_billing_id=billing_id, merchant_ref=merchant_ref) elif bankcard: card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) res = self.gateway.purchase(amount=amount, card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, merchant_ref=merchant_ref, enable_add_bill_card=1) else: raise ValueError("You must specify either a billing id or " + "a merchant reference") return self._handle_response(PURCHASE, order_number, amount, res) def refund(self, order_number, amount, dps_txn_ref): """ Refund - Funds transferred immediately. Must be enabled as a special option. """ self._check_amount(amount) merchant_ref = self._get_merchant_reference(order_number, REFUND) res = self.gateway.refund(amount=amount, dps_txn_ref=dps_txn_ref, merchant_ref=merchant_ref) return self._handle_response(REFUND, order_number, amount, res) def validate(self, bankcard): """ Validation Transaction. Effects a $1.00 Auth to validate card details including expiry date. Often utilised with the EnableAddBillCard property set to 1 to automatically add to Billing Database if the transaction is approved. """ amount = 1.00 card_issue_date = self._format_card_date(bankcard.start_date) card_expiry_date = self._format_card_date(bankcard.expiry_date) res = self.gateway.validate(amount=amount, card_holder=bankcard.card_holder_name, card_number=bankcard.card_number, card_issue_date=card_issue_date, card_expiry=card_expiry_date, cvc2=bankcard.cvv, enable_add_bill_card=1) return self._handle_response(VALIDATE, None, amount, res)