Skip to content
This repository has been archived by the owner on Jan 25, 2018. It is now read-only.

Commit

Permalink
simple post to solitude and handle in the spartacus responses (bug 11…
Browse files Browse the repository at this point in the history
…40615 and 1149095)
  • Loading branch information
Andy McKay committed Mar 31, 2015
1 parent 04fc862 commit 2dca0bc
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Expand Up @@ -13,7 +13,7 @@ ADD requirements /pip/requirements

# Setting cwd to /pip ensures egg-links for git installed deps are created in /pip/src
WORKDIR /pip
RUN pip install -b /pip/build --download-cache /pip/cache --no-deps -r /pip/requirements/docker.txt
RUN pip install -b /pip/build --download-cache /pip/cache --no-deps -r /pip/requirements/docker.txt --find-links https://pyrepo.addons.mozilla.org/

ENV SPARTACUS_STATIC /spartacus
ENV SOLITUDE_URL http://solitude:2602
Expand Down
2 changes: 2 additions & 0 deletions lib/solitude/api.py
Expand Up @@ -27,10 +27,12 @@

class BuyerNotConfigured(Exception):
"""The buyer has not yet been configured for the payment."""
solitude = 'BUYER_NOT_CONFIGURED'


class SellerNotConfigured(Exception):
"""The seller has not yet been configued for the payment."""
soltiude = 'SELLER_NOT_CONFIGURED'


class SolitudeAPI(SlumberWrapper):
Expand Down
7 changes: 5 additions & 2 deletions lib/solitude/constants.py
Expand Up @@ -11,10 +11,13 @@
STATUS_COMPLETED = 1
STATUS_CHECKED = 2
STATUS_RECEIVED = 3
STATUS_FAILED = 4
STATUS_FAILED = 4 # Failed at the payment provider.
STATUS_CANCELLED = 5
STATUS_STARTED = 6
STATUS_ERRORED = 7 # Failed at the Mozilla end.

STATUS_ENDED = (STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED)
STATUS_ENDED = (STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED,
STATUS_ERRORED)
STATUS_RETRY_OK = (STATUS_FAILED, STATUS_CANCELLED)

ACCESS_PURCHASE = 1
Expand Down
4 changes: 4 additions & 0 deletions webpay/base/dev_messages.py
Expand Up @@ -15,6 +15,7 @@
BAD_REQUEST = 'BAD_REQUEST'
BAD_SIM_RESULT = 'BAD_SIM_RESULT'
BANGO_ERROR = 'BANGO_ERROR'
BUYER_NOT_CONFIGURED = 'BUYER_NOT_CONFIGURED'
BUYER_UUID_ALREADY_EXISTS = 'BUYER_UUID_ALREADY_EXISTS'
EXPIRED_JWT = 'EXPIRED_JWT'
FIELD_REQUIRED = 'FIELD_REQUIRED'
Expand All @@ -38,7 +39,9 @@
NO_DEFAULT_LOC = 'NO_DEFAULT_LOC'
NO_PAY_FAILED_FUNC = 'NO_PAY_FAILED_FUNC'
NO_PAY_SUCCESS_FUNC = 'NO_PAY_SUCCESS_FUNC'
NO_PUBLICID_IN_JWT = 'NO_PUBLICID_IN_JWT'
NO_SIM_REASON = 'NO_SIM_REASON'
NO_VALID_SELLER = 'NO_VALID_SELLER'
NOTICE_ERROR = 'NOTICE_ERROR'
NOTICE_EXCEPTION = 'NOTICE_EXCEPTION'
PAY_DISABLED = 'PAY_DISABLED'
Expand All @@ -55,6 +58,7 @@
REVERIFY_MISSING_PROVIDER = 'REVERIFY_MISSING_PROVIDER'
REVERIFY_MISSING_URL = 'REVERIFY_MISSING_URL'
REVERIFY_TIMEOUT = 'REVERIFY_TIMEOUT'
SELLER_NOT_CONFIGURED = 'SELLER_NOT_CONFIGURED'
SIM_DISABLED = 'SIM_DISABLED'
SIM_ONLY_KEY = 'SIM_ONLY_KEY'
SIMULATE_FAIL = 'SIMULATE_FAIL'
Expand Down
3 changes: 3 additions & 0 deletions webpay/base/utils.py
Expand Up @@ -69,6 +69,7 @@ def system_error(request, **kw):

def custom_error(request, user_message, code=msg.UNEXPECTED_ERROR, status=400):
error = {'error': user_message, 'error_code': code}
print request.META.get('HTTP_ACCEPT')
if 'application/json' in request.META.get('HTTP_ACCEPT', ''):
return HttpResponse(
content=json.dumps(error),
Expand All @@ -82,6 +83,8 @@ def custom_error(request, user_message, code=msg.UNEXPECTED_ERROR, status=400):
'error_code': code,
'fxa_state': state,
'fxa_auth_url': fxa_url}
print '*' * 40
print ctx
return render(request, 'spa/index.html', ctx, status=status)

return render(request, 'error.html', error, status=status)
Expand Down
69 changes: 64 additions & 5 deletions webpay/pay/tasks.py
Expand Up @@ -18,6 +18,7 @@
from webpay.base import dev_messages
from webpay.base.utils import gmtime, uri_to_pk
from webpay.constants import TYP_CHARGEBACK, TYP_POSTBACK
from webpay.pay.errors import InvalidPublicID, NoValidSeller
from .constants import NOT_SIMULATED, SIMULATED_POSTBACK, SIMULATED_CHARGEBACK
from .utils import send_pay_notice, trans_id

Expand Down Expand Up @@ -157,7 +158,7 @@ def get_provider_seller_uuid(issuer_key, product_data, provider_names):
try:
public_id = product_data['public_id'][0]
except KeyError:
raise ValueError(
raise InvalidPublicID(
'Marketplace {key} did not put a '
'public_id in productData: {product_data}'.format(
key=settings.KEY, product_data=product_data
Expand All @@ -176,7 +177,7 @@ def get_provider_seller_uuid(issuer_key, product_data, provider_names):
.get_object_or_404())
generic_seller_uuid = seller['uuid']

for provider in provider_names:
for provider in []: #provider_names:
if product['seller_uuids'].get(provider, None) is not None:
provider_seller_uuid = product['seller_uuids'][provider]
log.info('Using provider seller uuid {s} for provider '
Expand All @@ -186,7 +187,7 @@ def get_provider_seller_uuid(issuer_key, product_data, provider_names):
return (ProviderHelper(provider), provider_seller_uuid,
generic_seller_uuid)

raise ValueError(
raise NoValidSeller(
'Unable to find a valid seller_uuid for public_id {public_id} '
'using providers: {providers}'.format(
public_id=public_id, providers=provider_names))
Expand Down Expand Up @@ -224,9 +225,13 @@ def start_pay(transaction_uuid, notes, user_uuid, provider_names, **kw):
"""
key = notes['issuer_key']
source = 'marketplace' if is_marketplace(key) else 'other'
pay = notes['pay_request']
network = notes.get('network', {})
product_data = urlparse.parse_qs(pay['request'].get('productData', ''))
provider_helper, provider_seller_uuid, generic_seller_uuid = (
None, None, None)

try:
(provider_helper,
provider_seller_uuid,
Expand Down Expand Up @@ -262,7 +267,7 @@ def start_pay(transaction_uuid, notes, user_uuid, provider_names, **kw):
icon_url=icon_url,
user_uuid=user_uuid,
application_size=application_size,
source='marketplace' if is_marketplace(key) else 'other',
source=source,
mcc=network.get('mcc'),
mnc=network.get('mnc')
)
Expand All @@ -275,13 +280,67 @@ def start_pay(transaction_uuid, notes, user_uuid, provider_names, **kw):
'status': constants.STATUS_PENDING
})
except Exception, exc:
etype, val, tb = sys.exc_info()
pay_error_handler(
error_type=etype,
provider_helper=provider_helper,
source=source,
uuid=transaction_uuid,
)
log.exception('while configuring payment for transaction {t}: '
'{exc.__class__.__name__}: {exc}'
.format(t=transaction_uuid, exc=exc))
etype, val, tb = sys.exc_info()
raise exc, None, tb


def pay_error_handler(
error_type,
provider_helper,
source,
uuid):
"""
Record a transaction error into solitude. At this point the transaction
may, or may not exist in solitude.
Unfortunately many things (buyer, seller, seller_product) are buried inside
start_transaction, which might need its own wrapper.
"""
pk = None
api = client.slumber.generic.transaction
try:
pk = api.get_object_or_404(uuid=uuid)
except ObjectDoesNotExist:
pass

# If the provider_helper is None, then no provider was found.
provider = None
# Convert the helper into a solitude constant.
if provider_helper:
provider = constants.PROVIDERS[provider_helper.name]

if pk:
# For a transaction to exist, everything up to creating the
# transaction in the payment provider has worked and something post
# that (eg in getting and patching the transaction) has failed.
#
# This should be pretty unusual.
log.info('Updating transaction as error for: {0}'.format(uuid))
api(pk).patch({
'status_reason': 'POST_CREATION_FAILURE',
'status': constants.STATUS_ERRORED,
})

else:
log.info('Creating transaction error for: {0}'.format(uuid))
api.post({
'provider': provider,
'status_reason': getattr(error_type, 'solitude', 'UNKNOWN'),
'source': source,
'status': constants.STATUS_ERRORED,
'uuid': uuid
})


@task(**notify_kw)
@use_master
def payment_notify(transaction_uuid, **kw):
Expand Down
56 changes: 46 additions & 10 deletions webpay/pay/tests/test_tasks.py
Expand Up @@ -26,6 +26,7 @@
from webpay.base.utils import gmtime
from webpay.constants import TYP_CHARGEBACK, TYP_POSTBACK
from webpay.pay import tasks
from webpay.pay.errors import InvalidPublicID
from webpay.pay.samples import JWTtester

from .test_views import sample
Expand Down Expand Up @@ -591,14 +592,15 @@ def test_bango_used_when_boku_not_supported(self):
)

@raises(api.SellerNotConfigured)
def test_no_seller(self):
@mock.patch('webpay.pay.tasks.pay_error_handler')
def test_no_seller(self, pay_error_handler):
raise SkipTest
self.mkt.webpay.prices.return_value = self.prices
self.solitude.generic.seller.get.return_value = {
'meta': {'total_count': 0}
}
self.start()
#eq_(self.get_trans().status, TRANS_STATE_FAILED)
assert pay_error_handler.is_called

def test_transaction_called(self):
self.solitude.generic.transaction.get_object.return_value = {
Expand Down Expand Up @@ -656,14 +658,12 @@ def test_price_fails(self, get_price):
self.start()

@raises(RuntimeError)
def test_exception_fails_transaction(self):
raise SkipTest
self.solitude.slumber.generic.seller.get.side_effect = RuntimeError
@mock.patch('webpay.pay.tasks.pay_error_handler')
def test_exception_fails_transaction(self, pay_error_handler):
self.solitude.generic.seller.get_object_or_404.side_effect = (
RuntimeError)
self.start()
#trans = self.get_trans()
# Currently solitude doesn't have the concept of a failed transaction.
# Perhaps we should add that?
#eq_(trans.status, TRANS_STATE_FAILED)
assert pay_error_handler.is_called

@mock.patch.object(settings, 'KEY', 'marketplace-domain')
def test_pay_url_saved(self):
Expand Down Expand Up @@ -715,7 +715,7 @@ def test_marketplace_wrong_application_size(self):
args = self.solitude.bango.billing.post.call_args
eq_(args[0][0]['application_size'], None)

@raises(ValueError)
@raises(InvalidPublicID)
@mock.patch.object(settings, 'KEY', 'marketplace-domain')
@mock.patch('webpay.pay.tasks.client')
def test_marketplace_missing_public_id(self, cli):
Expand All @@ -724,6 +724,42 @@ def test_marketplace_missing_public_id(self, cli):
self.start()


class TestStartPayError(BaseStartPay):

def setUp(self):
super(TestStartPayError, self).setUp()

p = mock.patch('lib.solitude.api.client.slumber')
self.solitude = p.start()
self.addCleanup(p.stop)

self.data = {
'error_type': InvalidPublicID,
'provider_helper': api.ProviderHelper('bango'),
'source': 'marketplace',
'uuid': 'some:uid',
}

def test_no_transaction(self):
self.solitude.generic.transaction.get_object_or_404.side_effect = (
ObjectDoesNotExist)
tasks.pay_error_handler(**self.data)
self.solitude.generic.transaction.post.assert_called_with({
'error_type': 'NO_PUBLICID_IN_JWT',
'provider': constants.PROVIDER_BANGO,
'source': 'marketplace',
'uuid': 'some:uid',
})

def test_transaction(self):
self.solitude.generic.transaction.get_object_or_404.returns = {
'resource_pk': 1,
'transaction_uuid': 'some:uid'
}
tasks.pay_error_handler(**self.data)
assert self.solitude.generic.transaction.patch.is_called


class TestConfigureTransaction(BaseStartPay):

def setUp(self):
Expand Down
10 changes: 10 additions & 0 deletions webpay/pay/tests/test_views.py
Expand Up @@ -527,6 +527,16 @@ def test_start_not_ready(self):
eq_(data['url'], None)
eq_(data['status'], constants.STATUS_RECEIVED)

def test_start_errored(self):
self.fake_transaction(
status=constants.STATUS_ERRORED,
status_reason='NO_PUBLICID_IN_JWT'
)
res = self.client.get(self.start, HTTP_ACCEPT='application/json')
eq_(res.status_code, 400, res.content)
data = json.loads(res.content)
eq_(data['error_code'], 'NO_PUBLICID_IN_JWT')

def wait_ended_transaction(self, status):
with self.settings(VERBOSE_LOGGING=True):
self.fake_transaction(status=status)
Expand Down
12 changes: 10 additions & 2 deletions webpay/pay/views.py
Expand Up @@ -390,6 +390,7 @@ def wait_to_start(request):
log.info('immediately redirecting to payment URL {url} '
'for trans {tr}'.format(url=url, tr=trans))
return http.HttpResponseRedirect(url)

return render(request, 'pay/wait-to-start.html')


Expand All @@ -402,7 +403,7 @@ def trans_start_url(request):
"""
trans = None
trans_id = request.session.get('trans_id')
data = {'url': None, 'status': None}
data = {'url': None, 'status': None, 'provider': None}

if not trans_id:
log.error('trans_start_url(): no transaction ID in session')
Expand All @@ -412,7 +413,7 @@ def trans_start_url(request):
with statsd.timer('purchase.payment_time.get_transaction'):
trans = solitude.get_transaction(trans_id)
data['status'] = trans['status']
data['provider'] = constants.PROVIDERS_INVERTED[trans['provider']]
data['provider'] = constants.PROVIDERS_INVERTED.get(trans['provider'])
except ObjectDoesNotExist:
log.error('trans_start_url() transaction does not exist: {t}'
.format(t=trans_id))
Expand All @@ -427,6 +428,13 @@ def trans_start_url(request):
log.info('async call got payment URL {url} for trans {tr}'
.format(url=url, tr=trans))
data['url'] = url

if trans and trans['status'] == constants.STATUS_ERRORED:
statsd.incr('purchase.payment_time.errored')
log.exception('Purchase configuration failed: {0} with status {1}'
'with status {1}'.format(trans_id, trans['status']))
return system_error(request, code=getattr(msg, trans['status_reason']))

return data


Expand Down

0 comments on commit 2dca0bc

Please sign in to comment.