├── .baked ├── .curling ├── .gitignore ├── .gitmodules ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── autoclean ├── bin ├── crontab │ ├── crontab.tpl │ └── gen-crons.py ├── docker │ └── supervisor.conf └── docker_run.sh ├── docker └── README.md ├── docs ├── Makefile ├── _static │ ├── .gitkeep │ └── solitude.svg ├── _templates │ └── .gitkeep ├── build-github.zsh ├── conf.py ├── index.rst └── topics │ ├── auth.rst │ ├── bango.rst │ ├── braintree.rst │ ├── generic.rst │ ├── overall.rst │ ├── proxy.rst │ ├── security.rst │ ├── services.rst │ ├── setup.rst │ └── zippy.rst ├── fabfile.py ├── lib ├── __init__.py ├── bango │ ├── __init__.py │ ├── client.py │ ├── constants.py │ ├── errors.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── clean_statuses.py │ ├── models.py │ ├── serializers.py │ ├── templates │ │ └── bango │ │ │ ├── terms-layout.html │ │ │ └── terms │ │ │ └── en-US.html │ ├── tests │ │ ├── __init__.py │ │ ├── samples.py │ │ ├── test_client.py │ │ ├── test_commands.py │ │ ├── test_constants.py │ │ ├── test_forms.py │ │ ├── test_notification.py │ │ ├── test_resources.py │ │ ├── test_utils_lib.py │ │ └── utils.py │ ├── urls.py │ ├── utils.py │ ├── views │ │ ├── __init__.py │ │ ├── bank.py │ │ ├── base.py │ │ ├── billing.py │ │ ├── event.py │ │ ├── login.py │ │ ├── notification.py │ │ ├── package.py │ │ ├── premium.py │ │ ├── product.py │ │ ├── rating.py │ │ ├── refund.py │ │ ├── sbi.py │ │ └── status.py │ └── wsdl │ │ ├── prod │ │ ├── billing_configuration_service.wsdl │ │ ├── billing_configuration_v2_0.wsdl │ │ ├── direct_billing.wsdl │ │ ├── direct_billing_service.xsd │ │ ├── mozilla_exporter.wsdl │ │ └── token_checker.wsdl │ │ ├── readme.txt │ │ └── test │ │ ├── billing_configuration_service.wsdl │ │ ├── billing_configuration_v2_0.wsdl │ │ ├── direct_billing.wsdl │ │ ├── direct_billing_service.xsd │ │ ├── mozilla_exporter.wsdl │ │ └── token_checker.wsdl ├── brains │ ├── __init__.py │ ├── client.py │ ├── errors.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── braintree_config.py │ │ │ ├── braintree_reset.py │ │ │ ├── braintree_webhook.py │ │ │ └── samples │ │ │ ├── __init__.py │ │ │ └── webhooks.py │ ├── models.py │ ├── serializers.py │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── live │ │ │ └── __init__.py │ │ ├── test_base.py │ │ ├── test_buyer.py │ │ ├── test_client.py │ │ ├── test_customer.py │ │ ├── test_forms.py │ │ ├── test_management.py │ │ ├── test_models.py │ │ ├── test_paymethod.py │ │ ├── test_sale.py │ │ ├── test_subscription.py │ │ ├── test_token.py │ │ ├── test_transaction.py │ │ └── test_webhook.py │ ├── urls.py │ ├── views │ │ ├── __init__.py │ │ ├── buyer.py │ │ ├── customer.py │ │ ├── paymethod.py │ │ ├── sale.py │ │ ├── subscription.py │ │ ├── token.py │ │ ├── transaction.py │ │ └── webhook.py │ └── webhooks.py ├── buyers │ ├── __init__.py │ ├── constants.py │ ├── field.py │ ├── forms.py │ ├── models.py │ ├── serializers.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_api.py │ │ ├── test_forms.py │ │ └── test_models.py │ ├── urls.py │ └── views.py ├── provider │ ├── __init__.py │ ├── bango.py │ ├── client.py │ ├── errors.py │ ├── reference.py │ ├── serializers.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_bango.py │ │ ├── test_client.py │ │ ├── test_reference.py │ │ ├── test_serializer.py │ │ └── test_views.py │ ├── urls.py │ └── views.py ├── proxy │ ├── __init__.py │ ├── constants.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── sellers │ ├── __init__.py │ ├── constants.py │ ├── models.py │ ├── serializers.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_api.py │ │ └── utils.py │ ├── urls.py │ └── views.py ├── services │ ├── __init__.py │ ├── resources.py │ └── tests.py └── transactions │ ├── __init__.py │ ├── constants.py │ ├── forms.py │ ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── log.py │ ├── models.py │ ├── serializers.py │ ├── tests │ ├── __init__.py │ ├── test_api.py │ ├── test_commands.py │ ├── test_forms.py │ └── test_models.py │ ├── urls.py │ └── views.py ├── logs └── .keep ├── manage.py ├── migrations ├── 01-noop.sql ├── 02-add-buyers.sql ├── 03-add-sellers.sql ├── 04-create-transactions.sql ├── 05-add-related.sql ├── 06-add-sellers-data.sql ├── 07-add-sellers-bluevia.sql ├── 08-add-source-column.sql ├── 09-add-datetime.sql ├── 10-increase-columns.sql ├── 11-add-product.sql ├── 12-add-pin-to-buyers.sql ├── 13-rename-secret.sql ├── 14-add-seller-bango.sql ├── 15-add-product-bango.sql ├── 16-redo-transactions.sql ├── 17-fix-transactions.sql ├── 18-celery-tasks.sql ├── 19-fix-product-bango.sql ├── 20-fix-transactions.sql ├── 21-fix-seller-product.sql ├── 22-fix-product-bango.sql ├── 23-add-delayable.sql ├── 24-add-pin-confirmed-to-buyer.sql ├── 25-add-active.sql ├── 26-amount-optional.sql ├── 27-add-notes.sql ├── 28-add-reset-to-buyer.sql ├── 29-add-pin-locking.sql ├── 30-add-sbi-expiration.sql ├── 31-add-public-id.sql ├── 32-populate-public-id.py ├── 33-add-unique.sql ├── 34-seller-product-access.sql ├── 35-add-pin-was-locked-to-buyer.sql ├── 36-add-bango-status.sql ├── 37-add-counter-field.sql ├── 38-alter-uid-pay.sql ├── 39-add-transaction-log.sql ├── 40-add-region-carrier.sql ├── 41-add-seller-boku.sql ├── 42-transaction-pay-url.sql ├── 43-remove-merchant-id.sql ├── 44-add-product-boku.sql ├── 45-add-seller-to-transactions.sql ├── 46-remove-unique-constraint-for-boku.sql ├── 47-add-product-reference.sql ├── 48-add-email-to-buyer.sql ├── 49-add-reference-id.sql ├── 50-fix-seller-product-reference.sql ├── 51-make-provider-optional.sql ├── 52-add-error-reason.sql ├── 53-remove-paypal.sql ├── 54-add-braintree-buyer.sql ├── 55-add-payment-method.sql ├── 56-add-subscriptions.sql ├── 57-add-braintree-transaction.sql ├── 58-add-locale.sql ├── 59-buyer-optional.sql ├── 60-fix-subscription.sql ├── 61-bt-subsription-amount.sql ├── 62-authenticated-buyer.sql ├── 63-buyer-email-sig.sql ├── 64-populate-email-hash.py ├── __init__.py └── schematic_settings.py ├── requirements ├── compiled.txt ├── dev.txt ├── docs.txt ├── prod.txt └── test.txt ├── samples ├── __init__.py ├── bango-basic.py ├── lib.py ├── readme.txt └── zippy-basic.py ├── solitude ├── __init__.py ├── authentication.py ├── base.py ├── constants.py ├── errors.py ├── exceptions.py ├── fields.py ├── filter.py ├── locale │ ├── en_US │ │ └── LC_MESSAGES │ │ │ └── messages.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ └── messages.po │ └── templates │ │ └── LC_MESSAGES │ │ └── messages.pot ├── logger.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── push_s3.py │ │ └── refresh_wsdl.py ├── middleware.py ├── paginator.py ├── processor.py ├── related_fields.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── mock-aes-sample.keys-dist │ ├── sample.key │ ├── sites │ │ ├── __init__.py │ │ ├── altdev │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ │ ├── dev │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ │ ├── paymentsalt │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ │ ├── prod │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ │ ├── s3dev │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ │ └── stage │ │ │ ├── __init__.py │ │ │ ├── db.py │ │ │ └── proxy.py │ └── test.py ├── tests │ ├── __init__.py │ ├── live.py │ ├── resources.py │ ├── test.py │ ├── test_authentication.py │ ├── test_error.py │ ├── test_middleware.py │ └── test_processor.py ├── urls.py ├── utils.py └── views.py └── wsgi ├── __init__.py ├── playdoh.py └── proxy.py /.baked: -------------------------------------------------------------------------------- 1 | { 2 | "order": ["stdlib", "django", "3rd", "local", "relative"], 3 | "fallback": "local", 4 | "from_order": { 5 | "local": false 6 | }, 7 | "modules": { 8 | "stdlib": [ 9 | "abc", "anydbm", "argparse", "array", "ast", 10 | "asynchat", "asyncore", "asyncio", "atexit", "base64", "BaseHTTPServer", 11 | "bisect", "bz2", "calendar", "cgitb", "cmd", "codecs", "collections", 12 | "commands", "compileall", "ConfigParser", "contextlib", "Cookie", 13 | "copy", "cPickle", "cProfile", "cStringIO", "csv", "datetime", 14 | "dbhash", "dbm", "decimal", "difflib", "dircache", "dis", "doctest", 15 | "dumbdbm", "EasyDialogs", "exceptions", "filecmp", "fileinput", 16 | "fnmatch", "fractions", "functools", "gc", "gdbm", "getopt", 17 | "getpass", "gettext", "glob", "grp", "gzip", "hashlib", "heapq", 18 | "hmac", "imaplib", "imp", "inspect", "itertools", "json", "linecache", 19 | "locale", "logging", "mailbox", "math", "mhlib", "mmap", 20 | "multiprocessing", "operator", "optparse", "os", "os.path", "pdb", 21 | "pickle", "pipes", "pkgutil", "platform", "plistlib", "pprint", 22 | "profile", "pstats", "pwd", "pyclbr", "pydoc", "Queue", "random", 23 | "re", "readline", "resource", "rlcompleter", "robotparser", "sched", 24 | "select", "shelve", "shlex", "shutil", "signal", "SimpleXMLRPCServer", 25 | "site", "sitecustomize", "smtpd", "smtplib", "socket", "SocketServer", 26 | "sqlite3", "string", "StringIO", "struct", "subprocess", "sys", 27 | "sysconfig", "tabnanny", "tarfile", "tempfile", "textwrap", 28 | "threading", "time", "timeit", "trace", "traceback", "unittest", 29 | "urllib", "urllib2", "urlparse", "usercustomize", "uuid", "warnings", 30 | "weakref", "webbrowser", "whichdb", "xml", "xml.etree.ElementTree", 31 | "xmlrpclib", "zipfile", "zipimport", "zlib", "posixpath", 32 | "mimetypes", "stat" 33 | ], 34 | "3rd": [ 35 | "M2Crypto", "MySQLdb", "aesfield", 36 | "apiclient", "cef", 37 | "celery", "celery_tasktree", "celeryutils", "commonware", 38 | "csp", "dateutil", "django_statsd", "lxml", 39 | "mock", "mozpay", "nose", "oauth2", "oauth2client", 40 | "oauthlib", "requests", "slumber", "suds", 41 | "tastypie", "tastypie_services", 42 | "rest_framework", "django_paranoia", "django_filters", 43 | "commonware.log", "chardet", "cache_nuggets", "mobile_codes", 44 | "braintree", "payments_config" 45 | ], 46 | "relative" : [""], 47 | "django": ["django"] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.curling: -------------------------------------------------------------------------------- 1 | { 2 | "solitude:2602": 3 | { 4 | "key": "local-curling-client", 5 | "secret": "please change this" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | settings_local.py 2 | settings/local.py 3 | */settings/local.py 4 | *.py[co] 5 | *.sw[po] 6 | .coverage 7 | pip-log.txt 8 | docs/_gh-pages 9 | docker/* 10 | build.py 11 | build 12 | .DS_Store 13 | *-min.css 14 | *-all.css 15 | *-min.js 16 | *-all.js 17 | .noseids 18 | tmp/* 19 | *~ 20 | *.mo 21 | *.keys 22 | docs/_build 23 | logs/* 24 | supervisord.pid 25 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor"] 2 | path = vendor 3 | url = git://github.com/mozilla/playdoh-lib.git 4 | [submodule "vendor-local/django-statsd"] 5 | path = vendor-local/django-statsd 6 | url = https://github.com/andymckay/django-statsd.git 7 | [submodule "vendor-local/django-tastypie"] 8 | path = vendor-local/django-tastypie 9 | url = https://github.com/toastdriven/django-tastypie.git 10 | [submodule "vendor-local/django-mysql-aesfield"] 11 | path = vendor-local/django-mysql-aesfield 12 | url = git://github.com/andymckay/django-mysql-aesfield.git 13 | [submodule "vendor-local/raven"] 14 | path = vendor-local/raven 15 | url = git://github.com/dcramer/raven.git 16 | [submodule "vendor-local/simplejson"] 17 | path = vendor-local/simplejson 18 | url = git://github.com/simplejson/simplejson.git 19 | [submodule "vendor-local/pyjwt"] 20 | path = vendor-local/pyjwt 21 | url = git://github.com/rtilder/pyjwt.git 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - '2.7' 5 | install: 6 | - pip install --no-deps MySQL-Python==1.2.5 --use-mirrors 7 | - pip install --find-links https://pyrepo.stage.mozaws.net/ peep 8 | - peep install --no-deps -r requirements/test.txt -r requirements/docs.txt --find-links https://pyrepo.stage.mozaws.net/ 9 | - peep install --no-deps -r requirements/compiled.txt --find-links https://pyrepo.stage.mozaws.net/travis/ 10 | before_script: 11 | - mysql -e 'create database solitude;' 12 | - flake8 . --exclude=./docs/conf.py 13 | script: ./manage.py test --noinput -s && make -C docs/ html linkcheck 14 | notifications: 15 | irc: 16 | channels: 17 | - irc.mozilla.org#amo-bots 18 | on_success: change 19 | on_failure: always 20 | env: 21 | global: 22 | - secure: D4wiZ6EjJOgUf0AYdzSRDP153QEgv8g5jMMmwDYw2Jw7/vVqRddgqAYwAAxuB1d94Iux7BS2mFZzyYRcaOqwNkivu1aIp+pj8M7WCEXH4BxGEHPHM8gsIg6WstuWwO4JuINdwgslEs1D4/PYRq5oVI7O7zxvVzG7DJeQxP/79hw= 23 | - secure: gyV+8RH+6RQeoDbLFcSC9PL2nBYub1hIAZAJxBNwG5Z2Opah42m+b6k5otrFdsFVrQrmgA0D01s9u9Y11Ymf+ruagJa6ZIVP46aBxCRs5boa3QFIWB6l/RocWqJKZA6Z8XihSjeoQ+eXXTihyHUfl+HzltAqUr+WthO5eEGAiNc= 24 | - secure: Qjfu3W5kevg1zI2MlpIngoCt9mltQy7Ko0FrDEsXihSuXtAqsl6TcoJ5e0laYI0bnxWFohJwhWrH+R0+o5H2SnoIzb93u9xC/ytRQQ8zFFANWqflGmKPaxpVSt9hPhBOLVSusVwkPollpTdEr/ou77/qgU7p1CyPmVR6fDhsa88= 25 | matrix: 26 | - LIVE_TESTS= 27 | - LIVE_TESTS=live,!braintree 28 | - LIVE_TESTS=!live,braintree 29 | matrix: 30 | allow_failures: 31 | - env: LIVE_TESTS=!live,braintree 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Before contributing code, please note: 2 | 3 | * You agree to license your contributions under the [license](https://github.com/mozilla/solitude/blob/master/LICENSE). 4 | * Ask on the [dev-marketplace mailing list](https://lists.mozilla.org/listinfo/dev-marketplace) or in bugzilla before making changes. 5 | * Follow [PEP8](http://www.python.org/dev/peps/pep-0008/), [jshint](http://www.jshint.com/) and our other [style guide conventions](http://mozweb.readthedocs.org/en/latest/index.html). 6 | * Please write tests and read the docs on [solitude](http://solitude.readthedocs.org/en/latest/) and the [marketplace](http://marketplace.readthedocs.org/en/latest/). 7 | 8 | And thank you for contributing code. 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This is designed to be run from fig as part of a 2 | # Marketplace development environment. 3 | 4 | # NOTE: this is not provided for production usage. 5 | FROM mozillamarketplace/centos-mysql-mkt:latest 6 | 7 | RUN yum install -y supervisor bash-completion cronie && yum clean all 8 | 9 | ENV IS_DOCKER 1 10 | 11 | # Copy requirements over first to cache the build. 12 | COPY requirements /srv/solitude/requirements 13 | 14 | # Download this securely from pyprepo first. 15 | RUN pip install --no-deps --find-links https://pyrepo.stage.mozaws.net/ peep 16 | RUN peep install \ 17 | --no-deps \ 18 | -r /srv/solitude/requirements/dev.txt \ 19 | -r /srv/solitude/requirements/compiled.txt \ 20 | --find-links https://pyrepo.stage.mozaws.net/ 21 | 22 | # Ship the source in the container, its up to docker-compose to override it 23 | # if it wants to. 24 | COPY . /srv/solitude 25 | RUN cd /srv/solitude && git show -s --pretty="format:%h" > git-rev.txt 26 | 27 | # Technically this should be in supervisor.conf, if the value is placed there, 28 | # when you enter a bash prompt on the container this value is unset. Meaning 29 | # that tests, dbshell and other really useful commands fail. 30 | # 31 | # To compensate supervisor.conf sets this environment variable to a blank 32 | # string, proving that the solitude proxy can run without this value set. 33 | ENV SOLITUDE_DATABASE mysql://root:@mysql:3306/solitude 34 | EXPOSE 2602 35 | 36 | # Preserve bash history across image updates. 37 | # This works best when you link your local source code 38 | # as a volume. 39 | ENV HISTFILE /srv/solitude/docker/bash_history 40 | # Configure bash history. 41 | ENV HISTSIZE 50000 42 | ENV HISTIGNORE ls:exit:"cd .." 43 | # This prevents dupes but only in memory for the current session. 44 | ENV HISTCONTROL erasedups 45 | 46 | # Add in the cron jobs. 47 | RUN mkdir -p /var/log/solitude/transactions/ 48 | RUN python /srv/solitude/bin/crontab/gen-crons.py -w /srv/solitude -p python --dir /var/log | crontab - 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include */*/templates *.* 2 | recursive-include */locale *.* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 2 | [![Stories in Ready](https://badge.waffle.io/mozilla/solitude.png?label=ready&title=Ready)](https://waffle.io/mozilla/solitude) 3 | [![Build Status](https://travis-ci.org/mozilla/solitude.svg?branch=master)](https://travis-ci.org/mozilla/solitude) 4 | 5 | Solitude is the Payment Server for processing marketplace and addons payments. 6 | 7 | For more read our docs: http://readthedocs.org/docs/solitude/en/latest/ 8 | 9 | Please note: this project is currently unmaintained and is not (or soon will not) be in active use by Mozilla. 10 | -------------------------------------------------------------------------------- /autoclean: -------------------------------------------------------------------------------- 1 | find . -name '*.py' -exec baked -i {} \; 2 | find . -name '*.py' -exec autopep8 -i {} \; 3 | flake8 . --exclude=docs,solitude/settings/local.py 4 | -------------------------------------------------------------------------------- /bin/crontab/crontab.tpl: -------------------------------------------------------------------------------- 1 | # 2 | # %(header)s 3 | # 4 | 5 | MAILTO=amo-developers@mozilla.org 6 | 7 | HOME=/tmp 8 | 9 | # Every 10 minutes run the stats log for today so we can see progress. 10 | */10 * * * * %(django)s log --type=stats --today %(dir)s 11 | 12 | # Once per day, generate stats log for yesterday so that we have a final log. 13 | 05 0 * * * %(django)s log --type=stats %(dir)s 14 | 15 | # Once per day, generate revenue log for monolith for yesterday. 16 | 10 0 * * * %(django)s log --type=revenue %(dir)s 17 | 18 | # Once per day, clean statuses older than BANGO_STATUSES_LIFETIME setting. 19 | 35 0 * * * %(django)s clean_statuses 20 | 21 | MAILTO=root 22 | -------------------------------------------------------------------------------- /bin/crontab/gen-crons.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from optparse import OptionParser 4 | 5 | HEADER = '!!AUTO-GENERATED!! Edit bin/crontab/crontab.tpl instead.' 6 | TEMPLATE = open(os.path.join(os.path.dirname(__file__), 'crontab.tpl')).read() 7 | 8 | 9 | def main(): 10 | parser = OptionParser() 11 | parser.add_option('-w', '--webapp', 12 | help='Location of web app (required)') 13 | parser.add_option('-u', '--user', 14 | help=('Prefix cron with this user. ' 15 | 'Only define for cron.d style crontabs.')) 16 | parser.add_option('-p', '--python', default='/usr/bin/python2.6', 17 | help='Python interpreter to use.') 18 | parser.add_option("-d", "--deprecations", default=False, 19 | help="Show deprecation warnings") 20 | parser.add_option('--dir', default=None, 21 | help='Output log directory') 22 | 23 | (opts, args) = parser.parse_args() 24 | 25 | if not opts.webapp: 26 | parser.error('-w must be defined') 27 | 28 | if not opts.deprecations: 29 | opts.python += ' -W ignore::DeprecationWarning' 30 | 31 | ctx = {'django': 'cd %s; %s manage.py' % (opts.webapp, opts.python)} 32 | 33 | if opts.user: 34 | for k, v in ctx.iteritems(): 35 | ctx[k] = '%s %s' % (opts.user, v) 36 | 37 | # Needs to stay below the opts.user injection. 38 | ctx['python'] = opts.python 39 | ctx['header'] = HEADER 40 | ctx['dir'] = ('--dir {}/solitude/transactions/'.format(opts.dir) 41 | if opts.dir else '') 42 | 43 | print TEMPLATE % ctx 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /bin/docker/supervisor.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=/srv/solitude/logs/supervisord.log 3 | 4 | [program:solitude] 5 | command=/bin/bash /srv/solitude/bin/docker_run.sh 6 | directory=/srv/solitude 7 | stopasgroup=true 8 | autostart=true 9 | redirect_stderr=true 10 | stdout_logfile=logs/docker.log 11 | stdout_logfile_maxbytes=1MB 12 | stopsignal=KILL 13 | environment= 14 | SOLITUDE_URL="http://solitude:2602" 15 | 16 | [inet_http_server] 17 | port=9001 18 | 19 | [rpcinterface:supervisor] 20 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 21 | -------------------------------------------------------------------------------- /bin/docker_run.sh: -------------------------------------------------------------------------------- 1 | # Startup script for running Solitude under Docker. 2 | 3 | # Check for mysql being up and running. 4 | mysqladmin -u root --host mysql_1 --silent --wait=30 ping || exit 1 5 | 6 | # Check database exists. If not create it first. 7 | mysql -u root --host mysql_1 -e 'use solitude;' 8 | if [ $? -ne 0 ]; then 9 | echo "Solitude database doesn't exist. Let's create it" 10 | mysql -u root --host mysql_1 -e 'create database solitude' 11 | fi 12 | 13 | # Lets always run the migrations. 14 | schematic migrations 15 | 16 | if [ "$BRAINTREE_MERCHANT_ID" ]; then 17 | echo "Braintree merchant ID found in environment, running braintree_config." 18 | python manage.py braintree_config 19 | fi 20 | 21 | python manage.py runserver 0.0.0.0:2602 22 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | This directory contains ephemeral artifacts from your running Docker container. 2 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/docs/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/build-github.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # A useful build script for projects hosted on github: 4 | # It can build your Sphinx docs and push them straight to your gh-pages branch. 5 | 6 | # Should be run from the docs directory: (cd docs && ./build-github.zsh) 7 | 8 | REPO=$(git config remote.origin.url) 9 | HERE=$(dirname $0) 10 | GH=$HERE/_gh-pages 11 | 12 | 13 | # Checkout the gh-pages branch, if necessary. 14 | if [[ ! -d $GH ]]; then 15 | git clone $REPO $GH 16 | pushd $GH 17 | git checkout -b gh-pages origin/gh-pages 18 | popd 19 | fi 20 | 21 | # Update and clean out the _gh-pages target dir. 22 | pushd $GH 23 | git pull && rm -rf * 24 | popd 25 | 26 | # Make a clean build. 27 | pushd $HERE 28 | make clean dirhtml 29 | 30 | # Move the fresh build over. 31 | cp -r _build/dirhtml/* $GH 32 | pushd $GH 33 | 34 | # Commit. 35 | git add . 36 | git commit -am "gh-pages build on $(date)" 37 | git push origin gh-pages 38 | 39 | popd 40 | popd 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | Solitude 3 | ======================================== 4 | 5 | *Please note:* this project is currently unmaintained and is not (or soon will not) be in active use by Mozilla. 6 | 7 | Solitude is a payments server for processing payments for Mozilla's Marketplace 8 | and Addons site. 9 | 10 | .. figure:: _static/solitude.svg 11 | :align: right 12 | :target: http://www.breweryvivant.com/ 13 | 14 | It provides a REST API for processing payments that you would plug into your 15 | site. We've implemented the APIs that we want to use for the Marketplace, not 16 | every API that the provider supports. 17 | 18 | Currently we support: 19 | 20 | * some `Bango `_ APIs 21 | * some `Braintree `_ APIs 22 | * some `Zippy `_ compliance 23 | 24 | In the past PayPal was supported, that has been removed. 25 | 26 | This project is based on **playdoh**. Mozilla's Playdoh is an open source 27 | web application template based on `Django `_. 28 | 29 | This document is available as a `PDF `_. 30 | 31 | Solitude is also a nice tasting beer from `Brewery Vivant `_. The logo is 32 | theirs. 33 | 34 | Contents 35 | -------- 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | 40 | topics/setup.rst 41 | topics/security.rst 42 | topics/overall.rst 43 | topics/auth.rst 44 | topics/generic.rst 45 | topics/bango.rst 46 | topics/braintree.rst 47 | topics/zippy.rst 48 | topics/proxy.rst 49 | topics/services.rst 50 | 51 | Indices and tables 52 | ------------------ 53 | 54 | * :ref:`genindex` 55 | * :ref:`modindex` 56 | * :ref:`search` 57 | -------------------------------------------------------------------------------- /docs/topics/auth.rst: -------------------------------------------------------------------------------- 1 | .. _auth.rst: 2 | 3 | Authentication 4 | ############## 5 | 6 | Most API requests can enforce zero-legged OAuth by having a shared key and 7 | secret on the servers. This allows solitude to check the client sending 8 | requests is allowed to do so. By default, this is `True`:: 9 | 10 | REQUIRE_OAUTH = True 11 | CLIENT_OAUTH_KEYS = { 12 | 'marketplace': 'please change this', 13 | 'webpay': 'please change this', 14 | } 15 | 16 | In development, you might want to connect with curl and other tools. For that 17 | alter the `REQUIRE_OAUTH` setting to `False`. 18 | 19 | .. note:: 20 | 21 | Service URLs do not require JWT encoding. 22 | -------------------------------------------------------------------------------- /docs/topics/security.rst: -------------------------------------------------------------------------------- 1 | .. _security: 2 | 3 | Security 4 | ######## 5 | 6 | Encryption 7 | ========== 8 | 9 | Currently we use `django-aesfield `_ 10 | to provide encryption on key fields. We'd recommend more levels of database 11 | encryption or file system encryption. 12 | 13 | The encryption uses AES to do this. 14 | 15 | Encrypted fields: 16 | 17 | * buyers email 18 | * sellers secret 19 | * bango signature 20 | 21 | The keys per field are mapped in settings. See :ref:`setup.rst` for more. 22 | 23 | Hashed fields 24 | ============= 25 | 26 | Fields are 27 | 28 | * buyers pin 29 | * buyers new pin 30 | 31 | Requests 32 | ======== 33 | 34 | All requests use OAuth 1.1 which signs the header using a secret key. Requests 35 | must be signed with that key or be rejected. 36 | -------------------------------------------------------------------------------- /docs/topics/services.rst: -------------------------------------------------------------------------------- 1 | .. _services.rst: 2 | 3 | Services 4 | ######## 5 | 6 | These are resources to provide information to clients about the status. 7 | 8 | .. http:get:: /services/request/ 9 | 10 | Echoes information back about the requet. 11 | 12 | **Response** 13 | 14 | :param authenticated: the OAuth key used to authenticate. 15 | :status 200: successful. 16 | 17 | .. http:get:: /services/status/ 18 | 19 | Returns information about things solitude needs. Useful for nagios. 20 | 21 | **Response** 22 | 23 | Example: 24 | 25 | .. code-block:: json 26 | 27 | { 28 | "meta": 29 | { 30 | "limit": 20, 31 | "next": null, 32 | "offset": 0, 33 | "previous": null, 34 | "total_count": 1 35 | }, 36 | "objects": 37 | [{ 38 | "cache": true, 39 | "db": true, 40 | "resource_uri": "", 41 | "settings": true 42 | }] 43 | } 44 | 45 | :status 200: successful. 46 | :status 500: theres a problem on the server. 47 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/__init__.py -------------------------------------------------------------------------------- /lib/bango/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/__init__.py -------------------------------------------------------------------------------- /lib/bango/errors.py: -------------------------------------------------------------------------------- 1 | class BangoError(Exception): 2 | 3 | def __init__(self, id, message): 4 | self.id = id 5 | self.message = message 6 | 7 | def __str__(self): 8 | return u'%s: %s' % (self.id, self.message) 9 | 10 | 11 | class AuthError(BangoError): 12 | 13 | """We've got the settings wrong on our end.""" 14 | 15 | 16 | class BangoAnticipatedError(BangoError): 17 | 18 | """ 19 | Something in the data we passed caused an error in the Bango end. 20 | 21 | This error is to denote that the error is going to be raised, but 22 | will be anticipated in some circumstances. This allows it to be caught 23 | and handled appropriately. 24 | """ 25 | 26 | 27 | class BangoUnanticipatedError(BangoError): 28 | 29 | """ 30 | Something in the data we passed caused Bango to return an error 31 | of not OK. 32 | 33 | This error is to denote that the error is going to be raised, but 34 | will be NOT anticipated. This allows it to be caught 35 | and handled appropriately. 36 | """ 37 | 38 | 39 | class BangoImmediateError(Exception): 40 | 41 | """ 42 | Something in the data we passed caused an error in Bango and rather 43 | than let it get trapped somewhere, we want to raise this immediately. 44 | """ 45 | 46 | 47 | class ProxyError(Exception): 48 | 49 | """The proxy returned something we didn't like.""" 50 | 51 | 52 | class ProcessError(Exception): 53 | 54 | def __init__(self, response): 55 | self.response = response 56 | -------------------------------------------------------------------------------- /lib/bango/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/management/__init__.py -------------------------------------------------------------------------------- /lib/bango/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/management/commands/__init__.py -------------------------------------------------------------------------------- /lib/bango/management/commands/clean_statuses.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | from optparse import make_option 3 | 4 | from django.conf import settings 5 | from django.core.management.base import BaseCommand 6 | 7 | from lib.bango.models import Status 8 | 9 | 10 | class Command(BaseCommand): 11 | help = 'Deletes all statuses with a lifetime greater than the parameter.' 12 | option_list = BaseCommand.option_list + ( 13 | make_option( 14 | '--lifetime', 15 | action='store_true', 16 | dest='lifetime', 17 | default=settings.BANGO_STATUSES_LIFETIME, 18 | help=('Set the maximum lifetime in days of cleaned statuses. ' 19 | 'Default: %s (BANGO_STATUSES_LIFETIME setting)' 20 | % settings.BANGO_STATUSES_LIFETIME) 21 | ), 22 | ) 23 | 24 | def handle(self, *args, **options): 25 | boundary_date = date.today() - timedelta(days=options['lifetime']) 26 | Status.objects.filter(created__lte=boundary_date).delete() 27 | -------------------------------------------------------------------------------- /lib/bango/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from .constants import STATUS_CHOICES, STATUS_UNKNOWN 4 | from lib.sellers.models import SellerProductBango 5 | from solitude.base import Model 6 | 7 | 8 | class Status(Model): 9 | status = models.IntegerField(choices=STATUS_CHOICES, 10 | default=STATUS_UNKNOWN) 11 | errors = models.TextField() 12 | seller_product_bango = models.ForeignKey(SellerProductBango, 13 | related_name='status') 14 | 15 | class Meta(Model.Meta): 16 | db_table = 'status_bango' 17 | -------------------------------------------------------------------------------- /lib/bango/templates/bango/terms-layout.html: -------------------------------------------------------------------------------- 1 | {% include terms %} 2 |
3 |

SBI Agreement

4 |

{{ sbi|linebreaksbr }}

5 | -------------------------------------------------------------------------------- /lib/bango/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/tests/__init__.py -------------------------------------------------------------------------------- /lib/bango/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | from django import test 4 | from django.core.management import call_command 5 | 6 | from nose.tools import eq_ 7 | 8 | import utils 9 | from lib.bango.models import Status 10 | 11 | 12 | class TestCleanStatusesCommand(test.TestCase): 13 | 14 | def setUp(self): 15 | """ 16 | Generates 3 statuses with different lifetimes of 5, 20 and 35 days 17 | to test both default and custom `lifetime` parameter. 18 | """ 19 | sellers = utils.make_sellers() 20 | for i in (5, 20, 35): 21 | status = Status.objects.create( 22 | seller_product_bango=sellers.product_bango, 23 | ) 24 | # Work around due to the `auto_now_add` option 25 | status.created = date.today() - timedelta(days=i) 26 | status.save() 27 | 28 | def test_command_call(self): 29 | with self.settings(BANGO_STATUSES_LIFETIME=30): 30 | call_command('clean_statuses') 31 | eq_(Status.objects.all().count(), 2) 32 | 33 | def test_lifetime_parameter(self): 34 | call_command('clean_statuses', **{'lifetime': 10}) 35 | eq_(Status.objects.all().count(), 1) 36 | -------------------------------------------------------------------------------- /lib/bango/tests/test_constants.py: -------------------------------------------------------------------------------- 1 | from ..constants import INVALID, match, OK 2 | 3 | 4 | def test_constants(): 5 | assert match('OK', OK) 6 | assert match('INVALID_FOO', INVALID) 7 | assert not match('INVALID', INVALID) 8 | -------------------------------------------------------------------------------- /lib/bango/tests/test_utils_lib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | from django import test 5 | 6 | from nose.tools import raises 7 | 8 | from lib.bango.utils import sign, terms, terms_directory, verify_sig 9 | 10 | 11 | class TestSigning(test.TestCase): 12 | 13 | def test_sign(self): 14 | sig = sign('123') 15 | assert verify_sig(sig, '123') 16 | 17 | def test_sign_unicode(self): 18 | sig = sign('123') 19 | assert verify_sig(sig, u'123') 20 | 21 | @raises(TypeError) 22 | def test_cannot_sign_non_ascii(self): 23 | sign(u'Ivan Krsti\u0107') 24 | 25 | def test_wrong_key(self): 26 | tmp = tempfile.NamedTemporaryFile(mode='w', delete=False) 27 | tmp.write('some secret') 28 | tmp.close() 29 | self.addCleanup(lambda: os.unlink(tmp.name)) 30 | with self.settings(AES_KEYS={'bango:signature': tmp.name}): 31 | sig = sign('123') 32 | assert not verify_sig(sig, '123') 33 | 34 | 35 | class TestTerms(test.TestCase): 36 | 37 | def setUp(self): 38 | self.fr = os.path.join(terms_directory, 'fr.html') 39 | 40 | def tearDown(self): 41 | if os.path.exists(self.fr): 42 | os.remove(self.fr) 43 | 44 | def test_en(self): 45 | assert 'Bango Developer Terms' in terms('sbi') 46 | 47 | def test_fr(self): 48 | with open(self.fr, 'w') as fr: 49 | fr.write('fr') 50 | assert 'fr' in terms('sbi', language='fr') 51 | 52 | def test_fallback(self): 53 | assert 'Bango Developer Terms' in terms('sbi', language='de') 54 | -------------------------------------------------------------------------------- /lib/bango/tests/utils.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from lib.sellers.models import (Seller, SellerBango, SellerProduct, 4 | SellerProductBango) 5 | 6 | Sellers = collections.namedtuple('Seller', 'seller bango product') 7 | SellerProducts = collections.namedtuple('SellerProduct', 8 | 'product_bango seller bango product ') 9 | 10 | 11 | def make_no_product(uuid='sample:uuid', bangoid='sample:bangoid'): 12 | seller = Seller.objects.create(uuid=uuid) 13 | bango = SellerBango.objects.create( 14 | seller=seller, 15 | package_id=1, 16 | admin_person_id=3, 17 | support_person_id=3, 18 | finance_person_id=4, 19 | ) 20 | product = SellerProduct.objects.create( 21 | seller=seller, 22 | external_id='xyz', 23 | ) 24 | return Sellers(seller, bango, product) 25 | 26 | 27 | def make_sellers(uuid='sample:uuid', bangoid='sample:bangoid'): 28 | no = make_no_product(uuid=uuid, bangoid=bangoid) 29 | product_bango = SellerProductBango.objects.create( 30 | seller_product=no.product, 31 | seller_bango=no.bango, 32 | bango_id=bangoid, 33 | ) 34 | return SellerProducts(product_bango, *no) 35 | -------------------------------------------------------------------------------- /lib/bango/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | 3 | from rest_framework.routers import SimpleRouter 4 | 5 | from lib.bango.views.bank import bank 6 | from lib.bango.views.billing import billing 7 | from lib.bango.views.event import event 8 | from lib.bango.views.login import login 9 | from lib.bango.views.notification import notification 10 | from lib.bango.views.package import PackageViewSet 11 | from lib.bango.views.premium import premium 12 | from lib.bango.views.product import ProductViewSet 13 | from lib.bango.views.rating import rating 14 | from lib.bango.views.refund import RefundViewSet 15 | from lib.bango.views.sbi import sbi 16 | from lib.bango.views.status import DebugViewSet, StatusViewSet 17 | 18 | bango_drf = SimpleRouter() 19 | bango_drf.register('status', StatusViewSet, base_name='status') 20 | bango_drf.register('debug', DebugViewSet, base_name='debug') 21 | bango_drf.register('product', ProductViewSet, base_name='product') 22 | bango_drf.register('package', PackageViewSet, base_name='package') 23 | bango_drf.register('refund', RefundViewSet, base_name='refund') 24 | 25 | urlpatterns = patterns( 26 | '', 27 | url(r'^login/$', login, name='bango.login'), 28 | url(r'^bank/$', bank, name='bank'), 29 | url(r'^premium/$', premium, name='premium'), 30 | url(r'^rating/$', rating, name='rating'), 31 | url(r'^billing/$', billing, name='billing'), 32 | url(r'^sbi/$', sbi, name='sbi'), 33 | url(r'^notification/$', notification, name='notification'), 34 | url(r'^event/$', event, name='event'), 35 | url(r'^', include(bango_drf.urls)), 36 | ) 37 | -------------------------------------------------------------------------------- /lib/bango/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import hashlib 3 | import hmac 4 | import os 5 | 6 | from django.conf import settings 7 | from django.template.loader import render_to_string 8 | 9 | from aesfield.default import lookup 10 | 11 | terms_directory = 'lib/bango/templates/bango/terms' 12 | 13 | 14 | def sign(msg): 15 | """ 16 | Sign a message with a Bango key. 17 | """ 18 | if isinstance(msg, unicode): 19 | try: 20 | msg = msg.encode('ascii') 21 | except UnicodeEncodeError, exc: 22 | raise TypeError('Cannot sign a non-ascii message. Error: %s' 23 | % exc) 24 | key = lookup(key='bango:signature') 25 | return hmac.new(key, msg, hashlib.sha256).hexdigest() 26 | 27 | 28 | def verify_sig(sig, msg): 29 | """ 30 | Verify the signature of a message using a Bango key. 31 | """ 32 | return str(sig) == sign(msg) 33 | 34 | 35 | def terms(sbi, language='en-US'): 36 | """ 37 | Look for a file containing the Bango terms, if not present, it will fall 38 | back to the en-US.html file. 39 | """ 40 | full = functools.partial(os.path.join, settings.ROOT, terms_directory) 41 | templates = (language + '.html', 'en-US.html') 42 | for template in templates: 43 | if os.path.exists(full(template)): 44 | break 45 | 46 | return render_to_string('bango/terms-layout.html', 47 | {'sbi': sbi, 'terms': full(template)}) 48 | -------------------------------------------------------------------------------- /lib/bango/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/bango/views/__init__.py -------------------------------------------------------------------------------- /lib/bango/views/bank.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.errors import BangoError, ProcessError 5 | from lib.bango.forms import CreateBankDetailsForm 6 | from lib.bango.serializers import SellerBangoOnly 7 | from lib.bango.views.base import BangoResource 8 | 9 | 10 | @api_view(['POST']) 11 | def bank(request): 12 | view = BangoResource() 13 | view.error_lookup = { 14 | 'INVALID_COUNTRYISO': 'bankAddressIso', 15 | } 16 | 17 | try: 18 | serial, form = view.process( 19 | serial_class=SellerBangoOnly, 20 | form_class=CreateBankDetailsForm, 21 | request=request) 22 | except ProcessError, exc: 23 | return exc.response 24 | 25 | data = form.cleaned_data 26 | data['packageId'] = serial.object['seller_bango'].package_id 27 | 28 | try: 29 | view.client('CreateBankDetails', data) 30 | except BangoError, exc: 31 | return view.client_errors(exc) 32 | 33 | return Response(form.cleaned_data) 34 | -------------------------------------------------------------------------------- /lib/bango/views/base.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from lib.bango.client import format_client_error, get_client 4 | from lib.bango.errors import ( 5 | BangoAnticipatedError, BangoImmediateError, BangoUnanticipatedError, 6 | ProcessError) 7 | from solitude.base import format_form_errors 8 | 9 | 10 | class BangoResource(object): 11 | 12 | def client(self, method, data, raise_on=None, client=None): 13 | """ 14 | Client to call the bango client and process errors in a way that 15 | is relevant to the form. If you pass in a list of errors, these will 16 | be treated as errors the callee is going to deal with and will not 17 | be returning ImmediateHttpResponses. Instead the callee will have to 18 | cope with these BangoAnticipatedErrors as appropriate. 19 | 20 | You can optionally pass in a client to override the default. 21 | """ 22 | raise_on = raise_on or [] 23 | try: 24 | return getattr(client or get_client(), method)(data) 25 | except BangoUnanticipatedError, exc: 26 | # It was requested that the error that was passed in 27 | # was actually anticipated, so let's raise that type of error. 28 | if exc.id in raise_on: 29 | raise BangoAnticipatedError(exc.id, exc.message) 30 | 31 | res = self.client_errors(exc) 32 | raise BangoImmediateError(format_form_errors(res)) 33 | 34 | def process(self, serial_class, form_class, request): 35 | form = form_class(request.DATA) 36 | if not form.is_valid(): 37 | raise ProcessError(Response(format_form_errors(form), status=400)) 38 | 39 | serial = serial_class(data=request.DATA) 40 | if not serial.is_valid(): 41 | raise ProcessError(Response(serial.errors, status=400)) 42 | 43 | return serial, form 44 | 45 | def client_errors(self, exc): 46 | key = getattr(self, 'error_lookup', {}).get(exc.id, '__all__') 47 | return format_client_error(key, exc) 48 | 49 | def form_errors(self, forms): 50 | return Response(format_form_errors(forms), status=400) 51 | -------------------------------------------------------------------------------- /lib/bango/views/event.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.forms import EventForm 5 | from lib.bango.views.base import BangoResource 6 | from lib.transactions.constants import STATUSES_INVERTED 7 | from solitude.base import log_cef 8 | from solitude.logger import getLogger 9 | 10 | log = getLogger('s.bango') 11 | 12 | 13 | @api_view(['POST']) 14 | def event(request): 15 | view = BangoResource() 16 | form = EventForm(request.DATA, request_encoding=request.encoding) 17 | if not form.is_valid(): 18 | log.info('Event invalid.') 19 | return view.form_errors(form) 20 | 21 | notification = form.cleaned_data['notification'] 22 | transaction = form.cleaned_data['transaction'] 23 | 24 | if notification['new_status'] != transaction.status: 25 | old_status = transaction.status 26 | transaction.status = notification['new_status'] 27 | transaction.save() 28 | 29 | log_cef('Transaction change success', request, severity=7, 30 | cs6Label='old', cs6=STATUSES_INVERTED[old_status], 31 | cs7Label='new', cs7=STATUSES_INVERTED[transaction.status]) 32 | log.info('Transaction {0} changed to {1} from {2}' 33 | .format(transaction.pk, transaction.status, 34 | old_status)) 35 | 36 | return Response(status=204) 37 | -------------------------------------------------------------------------------- /lib/bango/views/login.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.errors import BangoError 5 | from lib.bango.forms import GetEmailAddressesForm, GetLoginTokenForm 6 | from lib.bango.views.base import BangoResource 7 | 8 | 9 | @api_view(['POST']) 10 | def login(request): 11 | """ 12 | Retrieve package's infos from the package id 13 | to be able to later retrieve the authentication token 14 | given that we do not store any emails/persons ids. 15 | """ 16 | view = BangoResource() 17 | form = GetEmailAddressesForm(request.DATA) 18 | if not form.is_valid(): 19 | return view.form_errors(form) 20 | 21 | try: 22 | address = view.client('GetEmailAddresses', form.cleaned_data) 23 | except BangoError, exc: 24 | return view.client_errors(exc) 25 | 26 | form = GetLoginTokenForm(data={ 27 | 'packageId': form.cleaned_data['packageId'], 28 | 'emailAddress': address.adminEmailAddress, 29 | 'personId': address.adminPersonId, 30 | }) 31 | if not form.is_valid(): 32 | return view.form_errors(form) 33 | 34 | try: 35 | token = view.client('GetAutoAuthenticationLoginToken', 36 | form.cleaned_data) 37 | except BangoError, exc: 38 | return view.client_errors(exc) 39 | 40 | return Response({ 41 | 'authentication_token': token.authenticationToken, 42 | 'person_id': address.adminPersonId, 43 | 'email_address': address.adminEmailAddress, 44 | }) 45 | -------------------------------------------------------------------------------- /lib/bango/views/notification.py: -------------------------------------------------------------------------------- 1 | from django_statsd.clients import statsd 2 | from rest_framework.decorators import api_view 3 | from rest_framework.response import Response 4 | 5 | from lib.bango.constants import CANCEL, OK 6 | from lib.bango.forms import NotificationForm 7 | from lib.bango.views.base import BangoResource 8 | from lib.transactions.constants import (STATUS_CANCELLED, STATUS_COMPLETED, 9 | STATUS_FAILED, STATUSES_INVERTED) 10 | from solitude.base import log_cef 11 | from solitude.logger import getLogger 12 | 13 | log = getLogger('s.bango') 14 | 15 | 16 | @api_view(['POST']) 17 | def notification(request): 18 | view = BangoResource() 19 | form = NotificationForm(request, request.DATA) 20 | 21 | bill_conf_id = form.data.get('billing_config_id') 22 | log.info('Received notification for billing_config_id %r: ' 23 | 'bango_response_code: %r; bango_response_message: %r; ' 24 | 'bango_trans_id: %r; bango_token: %r; moz_transaction: %r; ' 25 | 'amount: %r; currency: %r' 26 | % (bill_conf_id, 27 | form.data.get('bango_response_code'), 28 | form.data.get('bango_response_message'), 29 | form.data.get('bango_trans_id'), 30 | form.data.get('bango_token'), 31 | form.data.get('moz_transaction'), 32 | form.data.get('amount'), 33 | form.data.get('currency'))) 34 | 35 | if not form.is_valid(): 36 | log.info(u'Notification invalid: %s' % bill_conf_id) 37 | return view.form_errors(form) 38 | 39 | trans = form.cleaned_data['moz_transaction'] 40 | states = {OK: ['completed', STATUS_COMPLETED], 41 | CANCEL: ['cancelled', STATUS_CANCELLED]} 42 | message, state = states.get(form.cleaned_data['bango_response_code'], 43 | ['failed', STATUS_FAILED]) 44 | 45 | log.info(u'Transaction %s: %s' % (message, trans.uuid)) 46 | statsd.incr('bango.notification.%s' % message) 47 | statsd.decr('solitude.pending_transactions') 48 | 49 | log_cef('Transaction change success', request, severity=7, 50 | cs6Label='old', cs6=STATUSES_INVERTED.get(trans.status), 51 | cs7Label='new', cs7=STATUSES_INVERTED.get(state)) 52 | 53 | trans.status = state 54 | # This is the id for the actual transaction, useful for refunds. 55 | trans.uid_support = form.cleaned_data['bango_trans_id'] 56 | # The price/currency may be empty for error notifications. 57 | trans.amount = form.cleaned_data['amount'] 58 | trans.currency = form.cleaned_data['currency'] 59 | # Set carrier and region. 60 | if form.cleaned_data.get('network'): 61 | trans.carrier = form.cleaned_data['carrier'] 62 | trans.region = form.cleaned_data['region'] 63 | 64 | trans.save() 65 | return Response(status=204) 66 | -------------------------------------------------------------------------------- /lib/bango/views/premium.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.constants import BANGO_ALREADY_PREMIUM_ENABLED 5 | from lib.bango.errors import ( 6 | BangoAnticipatedError, BangoError, BangoImmediateError, ProcessError) 7 | from lib.bango.forms import MakePremiumForm 8 | from lib.bango.serializers import SellerProductBangoOnly 9 | from lib.bango.views.base import BangoResource 10 | 11 | 12 | @api_view(['POST']) 13 | def premium(request): 14 | view = BangoResource() 15 | view.error_lookup = { 16 | 'INVALID_COUNTRYISO': 'currencyIso', 17 | } 18 | 19 | try: 20 | serial, form = view.process( 21 | serial_class=SellerProductBangoOnly, 22 | form_class=MakePremiumForm, 23 | request=request) 24 | except ProcessError, exc: 25 | return exc.response 26 | 27 | data = form.cleaned_data 28 | data['bango'] = serial.object['seller_product_bango'].bango_id 29 | if not data['bango']: 30 | # Note: that this error formatting seems quite inconsistent 31 | # with the rest of the errors. Something we should clean up. 32 | # https://github.com/mozilla/solitude/issues/349 33 | raise BangoImmediateError( 34 | {'seller_product_bango': 35 | ['Empty bango_id for: {0}' 36 | .format(serial.object['seller_product_bango'].pk)]} 37 | ) 38 | 39 | try: 40 | view.client('MakePremiumPerAccess', data, 41 | raise_on=[BANGO_ALREADY_PREMIUM_ENABLED]) 42 | except BangoAnticipatedError, exc: 43 | # This can occur and is expected, will return a 204 instead of 44 | # a 200 to distinguish in the client if you need to. 45 | return Response(status=204) 46 | except BangoError, exc: 47 | return view.client_errors(exc) 48 | 49 | return Response(form.cleaned_data) 50 | -------------------------------------------------------------------------------- /lib/bango/views/product.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from lib.bango.errors import ProcessError 4 | from lib.bango.forms import CreateBangoNumberForm 5 | from lib.bango.serializers import SellerProductBangoSerializer 6 | from lib.bango.views.base import BangoResource 7 | from lib.sellers.models import SellerProductBango 8 | from solitude.base import NonDeleteModelViewSet 9 | 10 | 11 | class ProductViewSet(NonDeleteModelViewSet, BangoResource): 12 | queryset = SellerProductBango.objects.filter() 13 | serializer_class = SellerProductBangoSerializer 14 | filter_fields = ('seller_product', 15 | 'seller_product__seller', 16 | 'seller_product__external_id') 17 | 18 | def create(self, request, *args, **kw): 19 | try: 20 | serial, form = self.process( 21 | serial_class=SellerProductBangoSerializer, 22 | form_class=CreateBangoNumberForm, 23 | request=request) 24 | except ProcessError, exc: 25 | return exc.response 26 | 27 | data = form.cleaned_data 28 | data['packageId'] = serial.object.seller_bango.package_id 29 | 30 | resp = self.client('CreateBangoNumber', data) 31 | 32 | product = SellerProductBango.objects.create( 33 | seller_bango=serial.object.seller_bango, 34 | seller_product=serial.object.seller_product, 35 | bango_id=resp.bango, 36 | ) 37 | 38 | serializer = SellerProductBangoSerializer(product) 39 | return Response(serializer.data, status=201) 40 | -------------------------------------------------------------------------------- /lib/bango/views/rating.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.errors import BangoError, BangoImmediateError, ProcessError 5 | from lib.bango.forms import UpdateRatingForm 6 | from lib.bango.serializers import SellerProductBangoOnly 7 | from lib.bango.views.base import BangoResource 8 | 9 | 10 | @api_view(['POST']) 11 | def rating(request): 12 | view = BangoResource() 13 | view.error_lookup = { 14 | 'INVALID_RATING': 'rating', 15 | 'INVALID_RATING_SCHEME': 'ratingScheme', 16 | } 17 | try: 18 | serial, form = view.process( 19 | serial_class=SellerProductBangoOnly, 20 | form_class=UpdateRatingForm, 21 | request=request) 22 | except ProcessError, exc: 23 | return exc.response 24 | 25 | data = form.cleaned_data 26 | data['bango'] = serial.object['seller_product_bango'].bango_id 27 | if not data['bango']: 28 | # Note: that this error formatting seems quite inconsistent 29 | # with the rest of the errors. Something we should clean up. 30 | raise BangoImmediateError( 31 | {'seller_product_bango': 32 | ['Empty bango_id for: {0}' 33 | .format(serial.object['seller_product_bango'].pk)]}) 34 | 35 | try: 36 | view.client('UpdateRating', data) 37 | except BangoError, exc: 38 | return view.client_errors(exc) 39 | 40 | return Response(form.cleaned_data) 41 | -------------------------------------------------------------------------------- /lib/bango/views/sbi.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.bango.constants import SBI_ALREADY_ACCEPTED 5 | from lib.bango.errors import BangoAnticipatedError 6 | from lib.bango.serializers import EasyObject, SBISerializer, SellerBangoOnly 7 | from lib.bango.utils import terms 8 | from lib.bango.views.base import BangoResource 9 | 10 | 11 | @api_view(['POST', 'GET']) 12 | def sbi(request): 13 | view = BangoResource() 14 | if request.method.upper() == 'GET': 15 | return sbi_get(view, request) 16 | return sbi_post(view, request) 17 | 18 | 19 | def sbi_post(view, request): 20 | serial = SellerBangoOnly(data=request.DATA) 21 | if not serial.is_valid(): 22 | return Response(serial.errors, status=400) 23 | 24 | data = {'packageId': serial.object['seller_bango'].package_id} 25 | try: 26 | res = view.client('AcceptSBIAgreement', data, 27 | raise_on=[SBI_ALREADY_ACCEPTED]) 28 | except BangoAnticipatedError, exc: 29 | if exc.id != SBI_ALREADY_ACCEPTED: 30 | raise 31 | 32 | res = view.client('GetAcceptedSBIAgreement', data) 33 | seller_bango = serial.object['seller_bango'] 34 | seller_bango.sbi_expires = res.sbiAgreementExpires 35 | seller_bango.save() 36 | 37 | obj = EasyObject( 38 | text='', 39 | valid=None, 40 | accepted=res.acceptedSBIAgreement, 41 | expires=res.sbiAgreementExpires 42 | ) 43 | return Response(SBISerializer(obj).data) 44 | 45 | 46 | def sbi_get(view, request): 47 | serial = SellerBangoOnly(data=request.DATA) 48 | if not serial.is_valid(): 49 | return Response(serial.errors, status=400) 50 | 51 | data = {'packageId': serial.object['seller_bango'].package_id} 52 | res = view.client('GetSBIAgreement', data) 53 | obj = EasyObject( 54 | text=terms(res.sbiAgreement), 55 | valid=res.sbiAgreementValidFrom, 56 | accepted=None, 57 | expires=None 58 | ) 59 | return Response(SBISerializer(obj).data) 60 | -------------------------------------------------------------------------------- /lib/bango/wsdl/prod/billing_configuration_service.wsdl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/bango/wsdl/readme.txt: -------------------------------------------------------------------------------- 1 | A cached version of the WSDL. To update run: 2 | 3 | ./manage.py refresh_wsdl 4 | 5 | To edit WSDL URLs, see solitude/management/commands/refresh_wsdl.py 6 | -------------------------------------------------------------------------------- /lib/brains/__init__.py: -------------------------------------------------------------------------------- 1 | # Called brains, because braintree is a python module for interacting with 2 | # braintree and was already taken. 3 | 4 | # Potential work around for https://bugs.python.org/issue7980, 5 | # Braintree uses strptime in their library and I was seeing intermittent 6 | # errors on parsing webhooks: 7 | # 8 | # File "...braintree/util/parser.py", line 42, in __convert_to_datetime 9 | # return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ") 10 | # AttributeError: _strptime 11 | 12 | import _strptime # noqa 13 | -------------------------------------------------------------------------------- /lib/brains/client.py: -------------------------------------------------------------------------------- 1 | from urlparse import urlparse 2 | 3 | from django.conf import settings 4 | from django.core.exceptions import ImproperlyConfigured 5 | 6 | import braintree 7 | from django_statsd.clients import statsd 8 | 9 | from solitude.logger import getLogger 10 | 11 | log = getLogger('s.brains') 12 | 13 | 14 | class AuthEnvironment(braintree.environment.Environment): 15 | 16 | def __init__(self, real): 17 | self._url = urlparse(settings.BRAINTREE_PROXY) 18 | self._real = real 19 | 20 | super(AuthEnvironment, self).__init__( 21 | self._url.hostname, self._url.port, '', 22 | self._url.scheme == 'https', None) 23 | 24 | 25 | AuthSandbox = AuthEnvironment(braintree.environment.Environment.Sandbox) 26 | AuthProduction = AuthEnvironment(braintree.environment.Environment.Production) 27 | 28 | 29 | class Http(braintree.util.http.Http): 30 | 31 | def http_do(self, verb, path, headers, body): 32 | # Tell solitude-auth where we really want this request to go to. 33 | headers['x-solitude-service'] = self.environment._real.base_url + path 34 | # Set the URL of the request to point to the auth server. 35 | path = self.environment._url.path 36 | 37 | with statsd.timer('solitude.braintree.api'): 38 | status, text = super(Http, self).http_do(verb, path, headers, body) 39 | statsd.incr('solitude.braintree.response.{0}'.format(status)) 40 | return status, text 41 | 42 | 43 | def get_client(): 44 | """ 45 | Use this to get the right client and communicate with Braintree. 46 | """ 47 | environments = { 48 | 'sandbox': AuthSandbox, 49 | 'production': AuthProduction, 50 | } 51 | 52 | if not settings.BRAINTREE_PROXY: 53 | raise ImproperlyConfigured('BRAINTREE_PROXY must be set.') 54 | 55 | if not settings.BRAINTREE_MERCHANT_ID: 56 | raise ImproperlyConfigured('BRAINTREE_MERCHANT_ID must be set.') 57 | 58 | braintree.Configuration.configure( 59 | environments[settings.BRAINTREE_ENVIRONMENT], 60 | settings.BRAINTREE_MERCHANT_ID, 61 | 'public key added by solitude-auth', 62 | 'private key added by solitude-auth', 63 | http_strategy=Http 64 | ) 65 | return braintree 66 | -------------------------------------------------------------------------------- /lib/brains/errors.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from django.core.exceptions import NON_FIELD_ERRORS 4 | 5 | from solitude.errors import ErrorFormatter 6 | from solitude.logger import getLogger 7 | 8 | log = getLogger('s.brains') 9 | 10 | 11 | class MockError(Exception): 12 | 13 | """ 14 | An attempt was made to use the mock, without a corresponding entry 15 | in the mocks dictionary. 16 | """ 17 | 18 | 19 | class BraintreeFormatter(ErrorFormatter): 20 | 21 | def format(self): 22 | errors = defaultdict(list) 23 | for error in self.error.result.errors.deep_errors: 24 | errors[error.attribute].append({ 25 | 'code': error.code, 26 | 'message': error.message 27 | }) 28 | 29 | # If there's not a verification object, 30 | # there will be a transaction object or neither. 31 | error = (self.error.result.credit_card_verification 32 | or self.error.result.transaction) 33 | if error: 34 | log.debug('Processing error: {}'.format(object)) 35 | 36 | if error.status.startswith('gateway'): 37 | field = NON_FIELD_ERRORS 38 | # I think these are two cases we care about 39 | # http://bit.ly/1FbxYCE 40 | if error.cvv_response_code in ['N', 'U']: 41 | field = 'cvv' 42 | 43 | errors[field].append({ 44 | 'code': error.gateway_rejection_reason, 45 | # There is no matching gateway_rejection_text. 46 | 'message': self.error.result.message 47 | }) 48 | 49 | # This covers JCB (failed) and all others (processor declined) 50 | elif error.status.startswith(('processor', 'failed')): 51 | errors[NON_FIELD_ERRORS].append({ 52 | 'code': error.processor_response_code, 53 | 'message': error.processor_response_text, 54 | }) 55 | 56 | # If we haven't found anything fall back to grabbing the message 57 | # at least. 58 | if not errors: 59 | errors[NON_FIELD_ERRORS].append({ 60 | 'code': 'unknown', 61 | 'message': self.error.result.message 62 | }) 63 | 64 | return {'braintree': dict(errors)} 65 | 66 | 67 | class BraintreeResultError(Exception): 68 | 69 | """ 70 | When an error occurs in the result that is not a standard error. 71 | """ 72 | status_code = 422 73 | formatter = BraintreeFormatter 74 | 75 | def __init__(self, result): 76 | self.result = result 77 | -------------------------------------------------------------------------------- /lib/brains/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/__init__.py -------------------------------------------------------------------------------- /lib/brains/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/commands/__init__.py -------------------------------------------------------------------------------- /lib/brains/management/commands/braintree_reset.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.core.management.base import BaseCommand, CommandError 4 | 5 | from lib.brains.models import BraintreePaymentMethod, BraintreeSubscription 6 | from lib.transactions.models import Transaction 7 | 8 | 9 | class Command(BaseCommand): 10 | help = 'Clean up braintree related data for development.' 11 | option_list = BaseCommand.option_list + ( 12 | make_option( 13 | '--clear-subscriptions', action='store_true', 14 | dest='clear_subscriptions', 15 | help='Remove all subscription data' 16 | ), 17 | make_option( 18 | '--clear-transactions', action='store_true', 19 | dest='clear_transactions', 20 | help='Remove all transaction data' 21 | ), 22 | make_option( 23 | '--clear-paymethods', action='store_true', 24 | dest='clear_paymethods', 25 | help=('Remove all saved payment methods. This also removes data ' 26 | 'tied to the payment methods.') 27 | ), 28 | ) 29 | 30 | def handle(self, *args, **options): 31 | did_something = False 32 | 33 | if options['clear_subscriptions']: 34 | did_something = True 35 | self._clear(BraintreeSubscription) 36 | if options['clear_paymethods']: 37 | did_something = True 38 | self._clear(BraintreePaymentMethod) 39 | if options['clear_transactions']: 40 | did_something = True 41 | self._clear(Transaction) 42 | 43 | if not did_something: 44 | raise CommandError('Nothing to do. Try specifying some options.') 45 | 46 | def _clear(self, model): 47 | count = model.objects.count() 48 | model.objects.all().delete() 49 | print '{} objects deleted: {}'.format(model.__name__, count) 50 | -------------------------------------------------------------------------------- /lib/brains/management/commands/samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/management/commands/samples/__init__.py -------------------------------------------------------------------------------- /lib/brains/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/tests/__init__.py -------------------------------------------------------------------------------- /lib/brains/tests/live/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/tests/live/__init__.py -------------------------------------------------------------------------------- /lib/brains/tests/test_buyer.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | 3 | from nose.tools import eq_ 4 | 5 | from lib.brains.tests.base import BraintreeTest, create_braintree_buyer 6 | 7 | 8 | class TestCustomer(BraintreeTest): 9 | 10 | def setUp(self): 11 | self.url = reverse('braintree:mozilla:buyer-list') 12 | 13 | def test_allowed(self): 14 | self.allowed_verbs(self.url, ['get']) 15 | 16 | def test_patch_ok(self): 17 | buyer, braintree_buyer = create_braintree_buyer() 18 | url = reverse('braintree:mozilla:buyer-detail', 19 | kwargs={'pk': braintree_buyer.pk}) 20 | self.client.patch(url, {'active': False}) 21 | res = self.client.get(url) 22 | eq_(res.json['braintree_id'], 'sample:id') 23 | 24 | def test_patch_readonly(self): 25 | buyer, braintree_buyer = create_braintree_buyer() 26 | url = reverse('braintree:mozilla:buyer-detail', 27 | kwargs={'pk': braintree_buyer.pk}) 28 | self.client.patch(url, {'active': False, 'braintree_id': 'foo'}) 29 | res = self.client.get(url) 30 | eq_(res.json['braintree_id'], 'sample:id') 31 | 32 | def test_lookup(self): 33 | create_braintree_buyer() 34 | buyer, braintree_buyer = create_braintree_buyer(braintree_id='f:id') 35 | res = self.client.get(self.url, {'buyer': buyer.pk}) 36 | eq_(res.json['meta']['total_count'], 1) 37 | eq_(res.json['objects'][0]['id'], braintree_buyer.pk) 38 | -------------------------------------------------------------------------------- /lib/brains/tests/test_client.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | 3 | from lib.brains.client import get_client, Http 4 | from lib.brains.tests.base import BraintreeTest 5 | 6 | 7 | class TestClient(BraintreeTest): 8 | 9 | def test_normal(self): 10 | with self.settings(BRAINTREE_PRIVATE_KEY='test-key'): 11 | assert isinstance( 12 | get_client().Configuration.instantiate()._http_strategy, 13 | Http) 14 | 15 | def test_missing(self): 16 | with self.settings(BRAINTREE_MERCHANT_ID=''): 17 | with self.assertRaises(ImproperlyConfigured): 18 | get_client() 19 | 20 | def test_missing_proxy(self): 21 | with self.settings(BRAINTREE_PROXY='', BRAINTREE_MERCHANT_ID='x'): 22 | with self.assertRaises(ImproperlyConfigured): 23 | get_client() 24 | -------------------------------------------------------------------------------- /lib/brains/tests/test_customer.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from braintree.customer import Customer 6 | from braintree.customer_gateway import CustomerGateway 7 | from braintree.successful_result import SuccessfulResult 8 | from nose.tools import eq_, ok_ 9 | 10 | from lib.brains.models import BraintreeBuyer 11 | from lib.brains.tests.base import ( 12 | BraintreeTest, create_braintree_buyer, create_buyer, error) 13 | 14 | 15 | def customer(**kw): 16 | customer = { 17 | 'id': 'customer-id', 18 | 'created_at': datetime.now(), 19 | 'updated_at': datetime.now() 20 | } 21 | customer.update(**kw) 22 | return Customer(None, customer) 23 | 24 | 25 | def successful_customer(**kw): 26 | return SuccessfulResult({'customer': customer(**kw)}) 27 | 28 | 29 | class TestCustomer(BraintreeTest): 30 | gateways = {'customer': CustomerGateway} 31 | 32 | def setUp(self): 33 | super(TestCustomer, self).setUp() 34 | self.url = reverse('braintree:customer') 35 | 36 | def test_buyer_doesnt_exist(self): 37 | res = self.client.post(self.url, data={'uuid': 'nope'}) 38 | eq_(res.status_code, 422) 39 | eq_(self.mozilla_error(res.json, 'uuid'), ['does_not_exist']) 40 | 41 | def test_braintree_buyer_exists(self): 42 | buyer, braintree_buyer = create_braintree_buyer() 43 | res = self.client.post(self.url, data={'uuid': buyer.uuid}) 44 | eq_(res.status_code, 422) 45 | eq_(self.mozilla_error(res.json, 'uuid'), ['already_exists']) 46 | 47 | def test_ok(self): 48 | self.mocks['customer'].create.return_value = successful_customer() 49 | 50 | buyer = create_buyer() 51 | res = self.client.post(self.url, data={'uuid': buyer.uuid}) 52 | eq_(res.status_code, 201) 53 | 54 | braintree_buyer = BraintreeBuyer.objects.get() 55 | eq_(res.json['mozilla']['resource_pk'], braintree_buyer.pk) 56 | eq_(res.json['mozilla']['braintree_id'], 'customer-id') 57 | eq_(res.json['braintree']['id'], 'customer-id') 58 | 59 | def test_error(self): 60 | self.mocks['customer'].create.return_value = error() 61 | 62 | buyer = create_buyer() 63 | res = self.client.post(self.url, data={'uuid': buyer.uuid}) 64 | 65 | ok_(not BraintreeBuyer.objects.exists()) 66 | eq_(res.status_code, 422) 67 | 68 | def test_no_uuid(self): 69 | res = self.client.post(self.url, data={}) 70 | eq_(res.status_code, 422) 71 | eq_(self.mozilla_error(res.json, 'uuid'), ['required']) 72 | -------------------------------------------------------------------------------- /lib/brains/tests/test_management.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import CommandError 2 | 3 | from braintree.plan import Plan 4 | from braintree.plan_gateway import PlanGateway 5 | from nose.tools import eq_, raises 6 | 7 | from lib.brains.client import get_client 8 | from lib.brains.management.commands import braintree_config as config 9 | from lib.brains.management.commands.braintree_config import ( 10 | BraintreePlanDoesNotExist 11 | ) 12 | from lib.brains.tests.base import BraintreeTest 13 | 14 | 15 | class TestManagement(BraintreeTest): 16 | gateways = {'plans': PlanGateway} 17 | 18 | def test_created(self): 19 | seller = config.get_or_create_seller('uuid:fxa') 20 | same_seller = config.get_or_create_seller('uuid:fxa') 21 | eq_(seller, same_seller) 22 | 23 | def test_created_product(self): 24 | seller = config.get_or_create_seller('uuid:fxa') 25 | seller_product = config.get_or_create_seller_product( 26 | external_id='some:product:uuid:concrete', 27 | public_id='some:product', 28 | seller=seller) 29 | eq_(seller_product, seller.product.get()) 30 | 31 | def get_plans(self, plan=None): 32 | # Note price is a string not a decimal or something useful: 33 | # https://github.com/braintree/braintree_python/issues/52 34 | plans = [ 35 | Plan(None, { 36 | 'billing_day_of_month': None, 37 | 'id': 'mozilla-concrete-mortar', 38 | 'price': '1', 39 | 'trial_period': None 40 | }), 41 | Plan(None, { 42 | 'billing_day_of_month': None, 43 | 'id': 'mozilla-concrete-brick', 44 | 'price': '10', 45 | 'trial_period': None 46 | }) 47 | ] 48 | if plan: 49 | plans.append(plan) 50 | self.mocks['plans'].all.return_value = plans 51 | return config.get_plans(get_client()) 52 | 53 | def test_plans(self): 54 | eq_(self.get_plans().keys(), 55 | [u'mozilla-concrete-mortar', u'mozilla-concrete-brick']) 56 | 57 | @raises(BraintreePlanDoesNotExist) 58 | def test_product_missing(self): 59 | config.product_exists(self.get_plans(), 'nope', 1) 60 | 61 | @raises(CommandError) 62 | def test_price_wrong(self): 63 | config.product_exists(self.get_plans(), 'mozilla-concrete-brick', 1) 64 | 65 | def test_plan_ok(self): 66 | config.product_exists(self.get_plans(), 'mozilla-concrete-brick', 10) 67 | 68 | @raises(CommandError) 69 | def test_trial_period(self): 70 | plan = Plan(None, { 71 | 'billing_day_of_month': None, 72 | 'id': 'trial', 73 | 'price': '1', 74 | 'trial_period': '1' 75 | }) 76 | config.product_exists(self.get_plans(plan), 'trial', 1) 77 | 78 | @raises(CommandError) 79 | def test_billing_day_of_month(self): 80 | plan = Plan(None, { 81 | 'billing_day_of_month': 1, 82 | 'id': 'day', 83 | 'price': '1', 84 | 'trial_period': None 85 | }) 86 | config.product_exists(self.get_plans(plan), 'day', 1) 87 | -------------------------------------------------------------------------------- /lib/brains/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from braintree.payment_method_gateway import PaymentMethodGateway 2 | from braintree.subscription_gateway import SubscriptionGateway 3 | from nose.tools import eq_ 4 | 5 | from lib.brains.tests.base import BraintreeTest, create_subscription 6 | from lib.brains.tests.test_paymethod import successful_method 7 | from lib.brains.tests.test_subscription import ( 8 | create_method_all, successful_subscription) 9 | 10 | 11 | class TestClose(BraintreeTest): 12 | gateways = { 13 | 'pay': PaymentMethodGateway, 14 | 'sub': SubscriptionGateway, 15 | } 16 | 17 | def setUp(self): 18 | super(TestClose, self).setUp() 19 | self.method, self.product = create_method_all() 20 | self.buyer = self.method.braintree_buyer.buyer 21 | self.sub = create_subscription(self.method, self.product) 22 | 23 | def test_no_buyer(self): 24 | self.buyer.delete() 25 | self.buyer.close() 26 | 27 | def test_close(self): 28 | self.mocks['pay'].delete.return_value = successful_method() 29 | self.mocks['sub'].cancel.return_value = successful_subscription() 30 | 31 | self.buyer.close() 32 | 33 | self.mocks['pay'].delete.assert_called_with(self.method.provider_id) 34 | self.mocks['sub'].cancel.assert_called_with(self.sub.provider_id) 35 | 36 | eq_(self.method.reget().active, False) 37 | eq_(self.sub.reget().active, False) 38 | 39 | def test_listens_signal(self): 40 | self.mocks['pay'].delete.return_value = successful_method() 41 | self.mocks['sub'].cancel.return_value = successful_subscription() 42 | 43 | self.buyer.close_signal.send( 44 | buyer=self.buyer, sender=self.buyer.__class__) 45 | 46 | def test_inactive_method(self): 47 | # If a method is inactive, then we still go and call cancel on 48 | # the subscription, just in case solitude is out of sync. 49 | self.method.active = False 50 | self.method.save() 51 | self.mocks['sub'].cancel.return_value = successful_subscription() 52 | 53 | self.buyer.close() 54 | 55 | self.mocks['sub'].cancel.assert_called_with(self.sub.provider_id) 56 | 57 | def test_inactive_subscription(self): 58 | self.sub.active = False 59 | self.sub.save() 60 | 61 | self.mocks['pay'].delete.return_value = successful_method() 62 | 63 | self.buyer.close() 64 | # self.mocks['sub'] not called, no need test the BraintreeTest 65 | # setup deals with this. 66 | -------------------------------------------------------------------------------- /lib/brains/tests/test_token.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | 3 | from braintree.client_token_gateway import ClientTokenGateway 4 | from nose.tools import eq_ 5 | 6 | from lib.brains.tests.base import BraintreeLiveTestCase, BraintreeTest 7 | 8 | 9 | class TestToken(BraintreeTest): 10 | gateways = {'client': ClientTokenGateway} 11 | 12 | def test_token(self): 13 | self.mocks['client'].generate.return_value = 'a-sample-token' 14 | res = self.client.post(reverse('braintree:token.generate')) 15 | eq_(res.json['token'], 'a-sample-token') 16 | 17 | 18 | class TestLiveToken(BraintreeLiveTestCase): 19 | 20 | def test_token(self): 21 | res = self.request.by_url('/braintree/token/generate/').post('') 22 | assert res['token'] 23 | -------------------------------------------------------------------------------- /lib/brains/tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from urllib import urlencode 3 | 4 | from django.core.urlresolvers import reverse 5 | 6 | from nose.tools import eq_ 7 | 8 | from lib.brains.models import BraintreeTransaction 9 | from lib.brains.tests.base import ( 10 | create_braintree_buyer, create_method, create_seller, create_subscription) 11 | from lib.transactions.models import Transaction 12 | from solitude.base import APITest 13 | 14 | 15 | class TestBraintreeTransaction(APITest): 16 | 17 | def setUp(self): 18 | self.buyer, self.braintree_buyer = create_braintree_buyer() 19 | self.method = create_method(self.braintree_buyer) 20 | self.seller, self.seller_product = create_seller() 21 | self.sub = create_subscription(self.method, self.seller_product) 22 | self.url = reverse('braintree:mozilla:transaction-list') 23 | self.transaction = Transaction.objects.create(uuid='some:uid', 24 | buyer=self.buyer) 25 | super(TestBraintreeTransaction, self).setUp() 26 | 27 | def test_allowed(self): 28 | self.allowed_verbs(self.url, ['get']) 29 | 30 | def create(self, **attributes): 31 | final_attributes = dict( 32 | paymethod=self.method, 33 | subscription=self.sub, 34 | transaction=self.transaction, 35 | billing_period_end_date=datetime.today() + timedelta(days=29), 36 | billing_period_start_date=datetime.today(), 37 | kind='sample', 38 | next_billing_date=datetime.today() + timedelta(days=30), 39 | next_billing_period_amount='10', 40 | ) 41 | final_attributes.update(attributes) 42 | return BraintreeTransaction.objects.create(**final_attributes) 43 | 44 | def get(self, **query): 45 | return self.client.get('{}?{}'.format(self.url, urlencode(query))) 46 | 47 | def test_get_transaction_by_pk(self): 48 | obj = self.create() 49 | eq_(self.client.get(obj.get_uri()).json['resource_pk'], obj.pk) 50 | 51 | def test_filter_by_buyer(self): 52 | # Create the first transaction: 53 | trans1 = self.create() 54 | 55 | # Create another transaction: 56 | gen_buyer2, bt_buyer2 = create_braintree_buyer(braintree_id='bt2') 57 | gen_trans2 = Transaction.objects.create(uuid='t2', buyer=gen_buyer2) 58 | paymethod2 = create_method(bt_buyer2) 59 | trans2 = self.create(paymethod=paymethod2, transaction=gen_trans2) 60 | 61 | objects = self.get( 62 | transaction__buyer__uuid=self.buyer.uuid).json['objects'] 63 | eq_(len(objects), 1, objects) 64 | eq_(objects[0]['resource_uri'], trans1.get_uri()) 65 | 66 | objects = self.get( 67 | transaction__buyer__uuid=gen_buyer2.uuid).json['objects'] 68 | eq_(len(objects), 1, objects) 69 | eq_(objects[0]['resource_uri'], trans2.get_uri()) 70 | 71 | def test_only_gets_are_allowed(self): 72 | obj = self.create() 73 | self.allowed_verbs(obj.get_uri(), ['get']) 74 | -------------------------------------------------------------------------------- /lib/brains/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from lib.brains.views import buyer, paymethod, subscription, transaction 6 | 7 | router = DefaultRouter() 8 | router.register(r'buyer', buyer.BraintreeBuyerViewSet, base_name='buyer') 9 | router.register(r'paymethod', paymethod.PaymentMethodViewSet, 10 | base_name='paymethod') 11 | router.register(r'subscription', subscription.SubscriptionViewSet, 12 | base_name='subscription') 13 | router.register(r'transaction', transaction.TransactionViewSet, 14 | base_name='transaction') 15 | 16 | urlpatterns = patterns( 17 | 'lib.brains.views', 18 | url(r'^mozilla/', include(router.urls, namespace='mozilla')), 19 | url(r'^token/generate/$', 'token.generate', name='token.generate'), 20 | url(r'^customer/$', 'customer.create', name='customer'), 21 | url(r'^paymethod/$', 'paymethod.create', name='paymethod'), 22 | url(r'^paymethod/delete/$', 'paymethod.delete', name='paymethod.delete'), 23 | url(r'^sale/$', 'sale.create', name='sale'), 24 | url(r'^subscription/$', 'subscription.create', name='subscription'), 25 | url(r'^subscription/cancel/$', 'subscription.cancel', 26 | name='subscription.cancel'), 27 | url(r'^subscription/paymethod/change/$', 'subscription.change', 28 | name='subscription.change'), 29 | url(r'^webhook/$', 'webhook.webhook', name='webhook'), 30 | ) 31 | -------------------------------------------------------------------------------- /lib/brains/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/brains/views/__init__.py -------------------------------------------------------------------------------- /lib/brains/views/buyer.py: -------------------------------------------------------------------------------- 1 | from lib.brains.models import BraintreeBuyer 2 | from lib.brains.serializers import LocalBuyer 3 | from solitude.base import NoAddModelViewSet 4 | 5 | 6 | class BraintreeBuyerViewSet(NoAddModelViewSet): 7 | queryset = BraintreeBuyer.objects.all() 8 | serializer_class = LocalBuyer 9 | filter_fields = ('buyer', 'active') 10 | -------------------------------------------------------------------------------- /lib/brains/views/customer.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.brains import serializers 5 | from lib.brains.client import get_client 6 | from lib.brains.errors import BraintreeResultError 7 | from lib.brains.forms import BuyerForm 8 | from lib.brains.models import BraintreeBuyer 9 | from solitude.errors import FormError 10 | from solitude.logger import getLogger 11 | 12 | log = getLogger('s.brains') 13 | 14 | 15 | @api_view(['POST']) 16 | def create(request): 17 | client = get_client().Customer 18 | form = BuyerForm(request.DATA) 19 | 20 | if not form.is_valid(): 21 | raise FormError(form.errors) 22 | 23 | result = client.create() 24 | if not result.is_success: 25 | log.warning('Error on creating Customer: {0}, {1}' 26 | .format(form.cleaned_data['uuid'], result.message)) 27 | raise BraintreeResultError(result) 28 | 29 | log.info('Braintree customer created: {0}'.format(result.customer.id)) 30 | 31 | braintree_buyer = BraintreeBuyer.objects.create( 32 | buyer=form.buyer, braintree_id=result.customer.id) 33 | 34 | log.info('Braintree buyer created: {0}'.format(braintree_buyer.pk)) 35 | 36 | res = serializers.Namespaced( 37 | mozilla=serializers.LocalBuyer(instance=braintree_buyer), 38 | braintree=serializers.Customer(instance=result.customer) 39 | ) 40 | return Response(res.data, status=201) 41 | -------------------------------------------------------------------------------- /lib/brains/views/paymethod.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.brains import serializers 5 | from lib.brains.client import get_client 6 | from lib.brains.errors import BraintreeResultError 7 | from lib.brains.forms import PaymentMethodForm, PayMethodDeleteForm 8 | from lib.brains.models import BraintreePaymentMethod 9 | from solitude.base import NoAddModelViewSet 10 | from solitude.constants import PAYMENT_METHOD_CARD 11 | from solitude.errors import FormError 12 | from solitude.logger import getLogger 13 | 14 | log = getLogger('s.brains') 15 | 16 | 17 | @api_view(['POST']) 18 | def delete(request): 19 | form = PayMethodDeleteForm(request.DATA) 20 | 21 | if not form.is_valid(): 22 | raise FormError(form.errors) 23 | 24 | solitude_method = form.cleaned_data['paymethod'] 25 | solitude_method.braintree_delete() 26 | solitude_method.active = False 27 | solitude_method.save() 28 | 29 | log.info('Payment method deleted from braintree: {}' 30 | .format(solitude_method.pk)) 31 | 32 | return Response({}, status=204) 33 | 34 | 35 | @api_view(['POST']) 36 | def create(request): 37 | client = get_client().PaymentMethod 38 | form = PaymentMethodForm(request.DATA) 39 | 40 | if not form.is_valid(): 41 | raise FormError(form.errors) 42 | 43 | buyer = form.buyer 44 | braintree_buyer = form.braintree_buyer 45 | result = client.create(form.braintree_data) 46 | 47 | if not result.is_success: 48 | log.warning('Error on creating Payment method: {0}, {1}' 49 | .format(buyer.uuid, result.message)) 50 | raise BraintreeResultError(result) 51 | 52 | braintree_method = result.payment_method 53 | log.info('PaymentMethod created for: {0}'.format(buyer.uuid)) 54 | 55 | solitude_method = BraintreePaymentMethod.objects.create( 56 | braintree_buyer=braintree_buyer, 57 | type=PAYMENT_METHOD_CARD, 58 | type_name=braintree_method.card_type, 59 | provider_id=braintree_method.token, 60 | truncated_id=result.payment_method.last_4 61 | ) 62 | log.info('Method {0} created.'.format(solitude_method.pk)) 63 | 64 | res = serializers.Namespaced( 65 | mozilla=serializers.LocalPayMethod(instance=solitude_method), 66 | braintree=serializers.PayMethod(instance=braintree_method) 67 | ) 68 | return Response(res.data, status=201) 69 | 70 | 71 | class PaymentMethodViewSet(NoAddModelViewSet): 72 | queryset = BraintreePaymentMethod.objects.filter() 73 | serializer_class = serializers.LocalPayMethod 74 | filter_fields = ('braintree_buyer', 'braintree_buyer__buyer__uuid', 75 | 'active') 76 | -------------------------------------------------------------------------------- /lib/brains/views/sale.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.brains.client import get_client 5 | from lib.brains.errors import BraintreeResultError 6 | from lib.brains.forms import SaleForm 7 | from lib.brains.models import BraintreeTransaction 8 | from lib.brains.serializers import LocalTransaction, Namespaced 9 | from lib.transactions import constants 10 | from lib.transactions.models import Transaction 11 | from lib.transactions.serializers import TransactionSerializer 12 | from solitude.errors import FormError 13 | from solitude.logger import getLogger 14 | 15 | log = getLogger('s.brains') 16 | 17 | 18 | @api_view(['POST']) 19 | def create(request): 20 | client = get_client().Transaction 21 | form = SaleForm(request.DATA) 22 | 23 | if not form.is_valid(): 24 | raise FormError(form.errors) 25 | 26 | result = client.sale(form.braintree_data) 27 | if not result.is_success: 28 | log.warning('Error on one-off sale: {}'.format(form.braintree_data)) 29 | raise BraintreeResultError(result) 30 | 31 | our_transaction = Transaction.objects.create( 32 | amount=result.transaction.amount, 33 | buyer=form.buyer, 34 | currency=result.transaction.currency_iso_code, 35 | provider=constants.PROVIDER_BRAINTREE, 36 | seller=form.seller_product.seller, 37 | seller_product=form.seller_product, 38 | status=constants.STATUS_CHECKED, 39 | type=constants.TYPE_PAYMENT, 40 | uid_support=result.transaction.id 41 | ) 42 | our_transaction.uuid = our_transaction.create_short_uid() 43 | our_transaction.save() 44 | log.info('Transaction created: {}'.format(our_transaction.pk)) 45 | 46 | braintree_transaction = BraintreeTransaction.objects.create( 47 | kind='submit_for_settlement', 48 | paymethod=form.cleaned_data['paymethod'], 49 | transaction=our_transaction, 50 | 51 | ) 52 | log.info('BraintreeTransaction created: {}' 53 | .format(braintree_transaction.pk)) 54 | 55 | res = Namespaced( 56 | braintree={}, # Not sure if there's anything useful to add here. 57 | mozilla={ 58 | 'braintree': LocalTransaction(braintree_transaction), 59 | 'generic': TransactionSerializer(our_transaction) 60 | } 61 | ) 62 | return Response(res.data, status=200) 63 | -------------------------------------------------------------------------------- /lib/brains/views/token.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | 4 | from lib.brains.client import get_client 5 | 6 | 7 | @api_view(['POST']) 8 | def generate(request): 9 | return Response({'token': get_client().ClientToken.generate()}) 10 | -------------------------------------------------------------------------------- /lib/brains/views/transaction.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from lib.brains.models import BraintreeTransaction 4 | from lib.brains.serializers import LocalTransaction 5 | from solitude.base import NoAddModelViewSet 6 | 7 | 8 | class TransactionViewSet(NoAddModelViewSet): 9 | queryset = BraintreeTransaction.objects.all() 10 | serializer_class = LocalTransaction 11 | filter_fields = ('transaction__buyer__uuid',) 12 | 13 | def update(self, *args, **kw): 14 | # Not sure what a patch to this object would do. 15 | return Response(status=405) 16 | -------------------------------------------------------------------------------- /lib/brains/views/webhook.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from braintree.util.xml_util import XmlUtil 4 | from braintree.webhook_notification import WebhookNotification 5 | from rest_framework.decorators import api_view 6 | from rest_framework.response import Response 7 | 8 | from lib.brains.client import get_client 9 | from lib.brains.forms import WebhookParseForm, WebhookVerifyForm 10 | from lib.brains.webhooks import Processor 11 | from solitude.errors import FormError 12 | from solitude.logger import getLogger 13 | 14 | log = getLogger('s.brains') 15 | debug_log = getLogger('s.webhook') 16 | 17 | 18 | def webhook(request): 19 | if request.method.lower() == 'get': 20 | return verify(request) 21 | return parse(request) 22 | 23 | 24 | @api_view(['POST']) 25 | def parse(request): 26 | form = WebhookParseForm(request.DATA) 27 | if not form.is_valid(): 28 | raise FormError(form.errors) 29 | 30 | # Parse the gateway without doing a validation on this server. 31 | # The validation has happened on the solitude-auth server. 32 | gateway = get_client().Configuration.instantiate().gateway() 33 | payload = base64.decodestring(form.cleaned_data['bt_payload']) 34 | attributes = XmlUtil.dict_from_xml(payload) 35 | parsed = WebhookNotification(gateway, attributes['notification']) 36 | 37 | log.info('Received webhook: {p.kind}.'.format(p=parsed)) 38 | debug_log.debug(parsed) 39 | 40 | processor = Processor(parsed) 41 | processor.process() 42 | data = processor.data 43 | return Response(data, status=200 if data else 204) 44 | 45 | 46 | @api_view(['GET']) 47 | def verify(request): 48 | form = WebhookVerifyForm(request.QUERY_PARAMS) 49 | if not form.is_valid(): 50 | raise FormError(form.errors) 51 | 52 | log.info('Received verification response: {r}'.format(r=form.response)) 53 | return Response(form.response) 54 | -------------------------------------------------------------------------------- /lib/buyers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/buyers/__init__.py -------------------------------------------------------------------------------- /lib/buyers/constants.py: -------------------------------------------------------------------------------- 1 | # Be careful changing these strings because webpay depends on them. 2 | BUYER_UUID_ALREADY_EXISTS = 'BUYER_UUID_ALREADY_EXISTS' 3 | FIELD_REQUIRED = 'FIELD_REQUIRED' 4 | PIN_4_NUMBERS_LONG = 'PIN_4_NUMBERS_LONG' 5 | PIN_ONLY_NUMBERS = 'PIN_ONLY_NUMBERS' 6 | -------------------------------------------------------------------------------- /lib/buyers/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.shortcuts import get_object_or_404 3 | 4 | from django_paranoia.forms import ParanoidForm 5 | 6 | from lib.buyers.constants import PIN_4_NUMBERS_LONG, PIN_ONLY_NUMBERS 7 | from lib.buyers.models import Buyer 8 | 9 | 10 | def clean_pin(pin): 11 | if pin is None or len(pin) == 0: 12 | return pin 13 | 14 | if not len(pin) == 4: 15 | raise forms.ValidationError(PIN_4_NUMBERS_LONG, code='invalid') 16 | 17 | if not pin.isdigit(): 18 | raise forms.ValidationError(PIN_ONLY_NUMBERS, code='invalid') 19 | 20 | return pin 21 | 22 | 23 | class PinForm(ParanoidForm): 24 | uuid = forms.CharField(required=True) 25 | pin = forms.CharField(required=True) 26 | 27 | def clean_uuid(self): 28 | self.cleaned_data['buyer'] = get_object_or_404( 29 | Buyer, 30 | uuid=self.cleaned_data.get('uuid')) 31 | return self.cleaned_data['uuid'] 32 | 33 | def clean_pin(self): 34 | return clean_pin(self.cleaned_data.get('pin')) 35 | -------------------------------------------------------------------------------- /lib/buyers/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/buyers/tests/__init__.py -------------------------------------------------------------------------------- /lib/buyers/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from lib.buyers.constants import PIN_4_NUMBERS_LONG, PIN_ONLY_NUMBERS 4 | from lib.buyers.forms import PinForm 5 | from lib.buyers.models import Buyer 6 | 7 | 8 | class BuyerFormTest(TestCase): 9 | 10 | def setUp(self): 11 | self.data = {'uuid': 'a:uuid'} 12 | Buyer.objects.create(uuid='a:uuid') 13 | 14 | def test_good_pin(self): 15 | self.data['pin'] = '1234' 16 | form = PinForm(self.data) 17 | assert form.is_valid() 18 | 19 | def test_too_long_pin(self): 20 | self.data['pin'] = '12345' 21 | form = PinForm(self.data) 22 | assert not form.is_valid() 23 | assert PIN_4_NUMBERS_LONG in form.errors['pin'] 24 | 25 | def test_too_short_pin(self): 26 | self.data['pin'] = '123' 27 | form = PinForm(self.data) 28 | assert not form.is_valid() 29 | assert PIN_4_NUMBERS_LONG in form.errors['pin'] 30 | 31 | def test_partially_numeric_pin(self): 32 | self.data['pin'] = '123a' 33 | form = PinForm(self.data) 34 | assert not form.is_valid() 35 | assert PIN_ONLY_NUMBERS in form.errors['pin'] 36 | 37 | def test_completely_alpha_pin(self): 38 | self.data['pin'] = 'asfa' 39 | form = PinForm(self.data) 40 | assert not form.is_valid() 41 | assert PIN_ONLY_NUMBERS in form.errors['pin'] 42 | -------------------------------------------------------------------------------- /lib/buyers/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from lib.buyers import views 6 | 7 | router = DefaultRouter() 8 | router.register(r'buyer', views.BuyerViewSet) 9 | 10 | urlpatterns = patterns( 11 | '', 12 | url(r'', include(router.urls)), 13 | url(r'^generic/buyer/(?P[\d]+)/close/$', views.close, name='close'), 14 | url(r'^confirm_pin', views.confirm_pin, name='confirm'), 15 | url(r'^verify_pin', views.verify_pin, name='verify'), 16 | url(r'^reset_confirm_pin', views.reset_confirm_pin, 17 | name='reset_confirm_pin') 18 | ) 19 | -------------------------------------------------------------------------------- /lib/provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/provider/__init__.py -------------------------------------------------------------------------------- /lib/provider/bango.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from lib.bango.forms import (CreateBangoNumberForm, MakePremiumForm, 4 | UpdateRatingForm) 5 | from lib.bango.serializers import SellerProductBangoSerializer 6 | from lib.bango.views.base import BangoResource 7 | from lib.sellers.models import SellerProductBango 8 | from solitude.base import BaseAPIView 9 | 10 | 11 | class ProductView(BaseAPIView): 12 | 13 | """ 14 | Override creating a product. 15 | """ 16 | 17 | def post(self, request, *args, **kwargs): 18 | view = BangoResource() 19 | form = CreateBangoNumberForm(request.DATA) 20 | if not form.is_valid(): 21 | return self.form_errors(form) 22 | 23 | serial = SellerProductBangoSerializer(data=request.DATA) 24 | if not serial.is_valid(): 25 | return Response(serial.errors, status=400) 26 | 27 | # Create the product. 28 | data = form.cleaned_data 29 | data['packageId'] = serial.object.seller_bango.package_id 30 | 31 | resp = view.client('CreateBangoNumber', data) 32 | 33 | product = SellerProductBango.objects.create( 34 | seller_bango=serial.object.seller_bango, 35 | seller_product=serial.object.seller_product, 36 | bango_id=resp.bango, 37 | ) 38 | 39 | # Make it premium. 40 | data = request.DATA.copy() 41 | data['bango'] = resp.bango 42 | data['price'] = '0.99' 43 | data['currencyIso'] = 'USD' 44 | 45 | form = MakePremiumForm(data) 46 | if not form.is_valid(): 47 | return self.form_errors(form) 48 | 49 | data = form.cleaned_data 50 | data['bango'] = resp.bango 51 | view.client('MakePremiumPerAccess', data) 52 | 53 | for rating, scheme in (['UNIVERSAL', 'GLOBAL'], 54 | ['GENERAL', 'USA']): 55 | # Make it global and US rating. 56 | data.update({'rating': rating, 'ratingScheme': scheme}) 57 | form = UpdateRatingForm(data) 58 | if not form.is_valid(): 59 | return self.form_errors(form) 60 | 61 | data = form.cleaned_data 62 | data['bango'] = resp.bango 63 | view.client('UpdateRating', data) 64 | 65 | return Response(SellerProductBangoSerializer(product).data) 66 | -------------------------------------------------------------------------------- /lib/provider/errors.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | 3 | 4 | class NoReference(Http404): 5 | 6 | """ 7 | The requested reference implementation did not exist. 8 | """ 9 | pass 10 | -------------------------------------------------------------------------------- /lib/provider/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/provider/tests/__init__.py -------------------------------------------------------------------------------- /lib/provider/tests/test_bango.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | 3 | from mock import patch 4 | from nose.tools import eq_ 5 | 6 | from lib.bango.client import ClientMock 7 | from lib.bango.constants import OK 8 | from lib.bango.tests import samples, utils 9 | from solitude.base import APITest 10 | 11 | 12 | class TestNope(APITest): 13 | 14 | def test(self): 15 | self.url = reverse('provider.bango.nope') 16 | eq_(self.client.get(self.url).status_code, 405) 17 | 18 | 19 | class TestProduct(APITest): 20 | uuid = 'sample:uuid' 21 | 22 | def setUp(self): 23 | self.objs = utils.make_no_product() 24 | self.url = reverse('provider.bango.product') 25 | 26 | def get_data(self): 27 | data = samples.good_bango_number 28 | data['seller_product'] = self.objs.product.get_uri() 29 | data['seller_bango'] = reverse('bango:package-detail', 30 | kwargs={'pk': self.objs.bango.pk}) 31 | return data 32 | 33 | def test(self): 34 | res = self.client.post(self.url, data=self.get_data()) 35 | eq_(res.status_code, 200, res.json) 36 | 37 | def test_form_fail(self): 38 | data = self.get_data() 39 | del data['seller_bango'] 40 | res = self.client.post(self.url, data=data) 41 | eq_(res.status_code, 400, res.json) 42 | 43 | @patch.object(ClientMock, 'mock_results') 44 | def test_calls(self, mock_results): 45 | mock_results.return_value = {'responseCode': OK, 46 | 'bango': '1'} 47 | res = self.client.post(self.url, data=self.get_data()) 48 | eq_(res.status_code, 200, res.json) 49 | eq_(len(mock_results.call_args_list), 4) 50 | -------------------------------------------------------------------------------- /lib/provider/tests/test_client.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from nose.tools import eq_, ok_ 4 | 5 | from ..client import Client, ClientProxy, get_client 6 | 7 | 8 | class TestClientObj(TestCase): 9 | 10 | def test_non_existant(self): 11 | client = Client('does-not-exist') 12 | eq_(client.api, None) 13 | 14 | def test_existing(self): 15 | config = { 16 | 'bob': { 17 | 'url': 'http://f.com', 18 | 'auth': {'key': 'k', 'secret': 's'} 19 | } 20 | } 21 | 22 | with self.settings(ZIPPY_CONFIGURATION=config): 23 | client = Client('bob') 24 | ok_(client.api) 25 | eq_(client.config, config['bob']) 26 | 27 | def test_proxy(self): 28 | with self.settings(ZIPPY_MOCK=False, ZIPPY_PROXY='http://blah/proxy'): 29 | client = get_client('bob') 30 | ok_(isinstance(client, ClientProxy)) 31 | eq_(client.api._store['base_url'], 'http://blah/proxy/bob') 32 | -------------------------------------------------------------------------------- /lib/provider/tests/test_serializer.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | from rest_framework import serializers 3 | 4 | from lib.provider.serializers import Remote 5 | from lib.sellers.models import Seller 6 | from lib.sellers.tests.utils import SellerTest 7 | 8 | 9 | class Sample(Remote): 10 | pk = serializers.CharField(max_length=100) 11 | remote = serializers.CharField(max_length=100) 12 | 13 | class Meta: 14 | model = Seller 15 | fields = ['pk'] 16 | remote = ['remote'] 17 | 18 | 19 | class TestSample(SellerTest): 20 | 21 | def test_remote(self): 22 | eq_(Sample(data={'local': 'f', 'remote': 'b'}).remote_data, 23 | {'remote': 'b'}) 24 | 25 | def test_restore(self): 26 | seller = self.create_seller() 27 | eq_(Sample().restore_object({'pk': seller.pk, 'remote': 'b'}).pk, 28 | seller.pk) 29 | -------------------------------------------------------------------------------- /lib/provider/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, patterns, url 2 | 3 | from rest_framework.routers import DefaultRouter 4 | 5 | from .bango import ProductView 6 | from .reference import SellerProductReference, SellerReference, Terms 7 | from .views import NotImplementedView, ProxyView 8 | 9 | bango_overrides = patterns( 10 | '', 11 | url(r'^product/$', ProductView.as_view(), name='provider.bango.product'), 12 | url(r'', NotImplementedView.as_view(), name='provider.bango.nope') 13 | ) 14 | 15 | reference = DefaultRouter() 16 | reference.register('sellers', SellerReference, base_name='sellers') 17 | reference.register('products', SellerProductReference, base_name='products') 18 | reference.register('terms', Terms, base_name='terms') 19 | 20 | urlpatterns = patterns( 21 | '', 22 | url(r'^bango/', include(bango_overrides)), 23 | url(r'^reference/', include(reference.urls, namespace='reference')), 24 | 25 | # The catch all for everything else that has not be viewsetted. 26 | url(r'^(?P\w+)/(?P\w+)/(?P[^/]+)?/?', 27 | ProxyView.as_view(), name='provider.api_view'), 28 | ) 29 | -------------------------------------------------------------------------------- /lib/proxy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/proxy/__init__.py -------------------------------------------------------------------------------- /lib/proxy/constants.py: -------------------------------------------------------------------------------- 1 | HEADERS_URL = 'x-solitude-url' 2 | HEADERS_TOKEN = 'x-solitude-token' 3 | HEADERS_URL_GET = 'HTTP_X_SOLITUDE_URL' 4 | HEADERS_TOKEN_GET = 'HTTP_X_SOLITUDE_TOKEN' 5 | -------------------------------------------------------------------------------- /lib/proxy/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/proxy/models.py -------------------------------------------------------------------------------- /lib/proxy/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | urlpatterns = patterns( 4 | '', 5 | url(r'^bango$', 'lib.proxy.views.bango', name='bango.proxy'), 6 | url(r'^provider/(?P\w+)/', 'lib.proxy.views.provider', 7 | name='provider.proxy'), 8 | ) 9 | -------------------------------------------------------------------------------- /lib/sellers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/sellers/__init__.py -------------------------------------------------------------------------------- /lib/sellers/constants.py: -------------------------------------------------------------------------------- 1 | from solitude.base import invert 2 | 3 | 4 | EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE = 'EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE' 5 | 6 | ACCESS_PURCHASE = 1 7 | ACCESS_SIMULATE = 2 8 | 9 | ACCESS_TYPES = { 10 | # The product can be purchased. 11 | 'purchase': ACCESS_PURCHASE, 12 | # The product can only go through a simulated purchase. 13 | 'simulate': ACCESS_SIMULATE, 14 | } 15 | 16 | ACCESS_CHOICES = invert(ACCESS_TYPES) 17 | -------------------------------------------------------------------------------- /lib/sellers/serializers.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ObjectDoesNotExist 2 | from django.core.urlresolvers import reverse 3 | 4 | from rest_framework import serializers 5 | 6 | from lib.bango.serializers import SellerBangoSerializer 7 | from lib.sellers.constants import EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE 8 | from lib.sellers.models import Seller, SellerBango, SellerProduct 9 | from solitude.base import BaseSerializer 10 | from solitude.related_fields import PathRelatedField 11 | 12 | 13 | class SellerSerializer(BaseSerializer): 14 | bango = PathRelatedField( 15 | view_name='bango:package-detail', 16 | read_only=True 17 | ) 18 | 19 | class Meta: 20 | model = Seller 21 | 22 | def transform_bango(self, obj, value): 23 | # This makes me so sad that we did this. Please not again. 24 | # https://github.com/mozilla/solitude/issues/343 25 | try: 26 | seller_bango = SellerBango.objects.get(seller=obj) 27 | return SellerBangoSerializer(seller_bango).data 28 | except ObjectDoesNotExist: 29 | return {} 30 | 31 | def resource_uri(self, pk): 32 | return reverse('generic:seller-detail', kwargs={'pk': pk}) 33 | 34 | 35 | class SellerProductSerializer(BaseSerializer): 36 | seller_uuids = serializers.CharField(source='supported_providers', 37 | read_only=True) 38 | seller = PathRelatedField( 39 | view_name='generic:seller-detail', 40 | lookup_field='pk' 41 | ) 42 | 43 | class Meta: 44 | model = SellerProduct 45 | # Note: fields are validated in this order, ensure that 46 | # external_id is after seller. 47 | fields = [ 48 | 'seller', 'access', 'resource_uri', 'resource_pk', 'secret', 49 | 'seller_uuids', 'public_id', 'external_id', 50 | ] 51 | 52 | def validate_external_id(self, attrs, source): 53 | value = attrs.get(source) 54 | seller = attrs.get('seller') 55 | 56 | qs = SellerProduct.objects.filter(external_id=value) 57 | if seller: 58 | qs = qs.filter(seller=seller) 59 | 60 | if self.object: 61 | qs = qs.exclude(pk=self.object.pk) 62 | 63 | if qs.exists(): 64 | raise serializers.ValidationError( 65 | EXTERNAL_PRODUCT_ID_IS_NOT_UNIQUE) 66 | 67 | return attrs 68 | 69 | def resource_uri(self, pk): 70 | return reverse('generic:sellerproduct-detail', kwargs={'pk': pk}) 71 | -------------------------------------------------------------------------------- /lib/sellers/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/sellers/tests/__init__.py -------------------------------------------------------------------------------- /lib/sellers/tests/utils.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from lib.sellers.models import Seller, SellerProduct 6 | from solitude.base import APITest 7 | 8 | 9 | class SellerTest(APITest): 10 | 11 | def create_seller(self, **kwargs): 12 | defaults = {'uuid': 'seller:' + str(uuid.uuid4())} 13 | defaults.update(kwargs) 14 | return Seller.objects.create(**defaults) 15 | 16 | def get_seller_uri(self, seller): 17 | return reverse('generic:seller-detail', kwargs={'pk': seller.pk}) 18 | 19 | def create_seller_product(self, seller=None, **kwargs): 20 | defaults = { 21 | 'seller': seller or self.create_seller(), 22 | 'public_id': 'public:' + str(uuid.uuid4()), 23 | 'external_id': 'external:' + str(uuid.uuid4()), 24 | } 25 | defaults.update(kwargs) 26 | 27 | return SellerProduct.objects.create(**defaults) 28 | 29 | def get_seller_product_uri(self, seller_product): 30 | return seller_product.get_uri() 31 | -------------------------------------------------------------------------------- /lib/sellers/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | 3 | from lib.sellers import views 4 | 5 | router = DefaultRouter() 6 | router.register(r'seller', views.SellerViewSet) 7 | router.register(r'product', views.SellerProductViewSet) 8 | 9 | urlpatterns = router.urls 10 | -------------------------------------------------------------------------------- /lib/sellers/views.py: -------------------------------------------------------------------------------- 1 | from lib.sellers.models import Seller, SellerProduct 2 | from lib.sellers.serializers import SellerProductSerializer, SellerSerializer 3 | from solitude.base import NonDeleteModelViewSet 4 | 5 | 6 | class SellerViewSet(NonDeleteModelViewSet): 7 | queryset = Seller.objects.all() 8 | serializer_class = SellerSerializer 9 | filter_fields = ('uuid', 'active') 10 | 11 | 12 | class SellerProductViewSet(NonDeleteModelViewSet): 13 | queryset = SellerProduct.objects.all() 14 | serializer_class = SellerProductSerializer 15 | filter_fields = ( 16 | 'external_id', 'public_id', 'seller__uuid', 'seller__active', 17 | 'seller' 18 | ) 19 | -------------------------------------------------------------------------------- /lib/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/services/__init__.py -------------------------------------------------------------------------------- /lib/services/tests.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.cache import cache 3 | from django.core.urlresolvers import reverse 4 | 5 | from mock import patch 6 | from nose.tools import eq_ 7 | 8 | from lib.services.resources import TestError 9 | from solitude.base import APITest 10 | 11 | 12 | @patch.object(settings, 'DEBUG', False) 13 | class TestStatus(APITest): 14 | 15 | def setUp(self): 16 | self.list_url = reverse('services.status') 17 | 18 | def failed(self, res, on): 19 | eq_(res.status_code, 500) 20 | assert False in res.json.values() 21 | 22 | @patch.object(cache, 'get', lambda x: None) 23 | def test_failure_status(self): 24 | res = self.client.get(self.list_url) 25 | self.failed(res, 'cache') 26 | 27 | # Note that Django will use the values in the settings, altering 28 | # CACHES right now will still work if your settings allow it. Urk. 29 | @patch('requests.get') 30 | @patch('lib.services.resources.StatusObject.test_cache') 31 | @patch('lib.services.resources.StatusObject.test_db') 32 | def test_proxy(self, test_db, test_cache, get): 33 | test_cache.return_value = True 34 | test_db.return_value = False 35 | with self.settings(SOLITUDE_PROXY=True, 36 | DATABASES={'default': {'ENGINE': ''}}, 37 | CACHES={}): 38 | res = self.client.get(self.list_url) 39 | eq_(res.status_code, 200, res.content) 40 | 41 | @patch('requests.get') 42 | @patch('lib.services.resources.StatusObject.test_cache') 43 | @patch('lib.services.resources.StatusObject.test_db') 44 | def test_proxy_db(self, test_db, test_cache, requests): 45 | test_db.return_value = False 46 | test_cache.return_value = False 47 | with self.settings(SOLITUDE_PROXY=True, 48 | DATABASES={'default': {'ENGINE': 'foo'}}, 49 | CACHES={}): 50 | self.failed(self.client.get(self.list_url), 'settings') 51 | 52 | 53 | class TestErrors(APITest): 54 | 55 | def test_throws_error(self): 56 | with self.assertRaises(TestError): 57 | self.client.get(reverse('services.error')) 58 | 59 | 60 | class TestNoop(APITest): 61 | 62 | def test_noop(self): 63 | eq_(self.client.get(reverse('services.request')).status_code, 200) 64 | -------------------------------------------------------------------------------- /lib/transactions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/__init__.py -------------------------------------------------------------------------------- /lib/transactions/constants.py: -------------------------------------------------------------------------------- 1 | from solitude.base import invert 2 | 3 | # Please see docs for an explanation of these. 4 | STATUS_PENDING = 0 5 | STATUS_COMPLETED = 1 6 | STATUS_CHECKED = 2 7 | STATUS_RECEIVED = 3 8 | STATUS_FAILED = 4 9 | STATUS_CANCELLED = 5 10 | # These are statuses that reflect the transactions state in solitude 11 | # as it is configured by the client. 12 | STATUS_STARTED = 6 13 | STATUS_ERRORED = 7 14 | 15 | STATUS_DEFAULT = STATUS_STARTED 16 | 17 | STATUSES = { 18 | 'cancelled': STATUS_CANCELLED, 19 | 'checked': STATUS_CHECKED, 20 | 'completed': STATUS_COMPLETED, 21 | 'failed': STATUS_FAILED, 22 | 'pending': STATUS_PENDING, 23 | 'received': STATUS_RECEIVED, 24 | 'started': STATUS_STARTED, 25 | 'errored': STATUS_ERRORED 26 | } 27 | STATUSES_INVERTED = dict((v, k) for k, v in STATUSES.items()) 28 | 29 | TYPE_PAYMENT = 0 30 | TYPE_REFUND = 1 31 | TYPE_REVERSAL = 2 32 | TYPE_REFUND_MANUAL = 3 33 | TYPE_REVERSAL_MANUAL = 4 34 | 35 | TYPE_DEFAULT = TYPE_PAYMENT 36 | 37 | TYPES = { 38 | 'payment': TYPE_PAYMENT, 39 | 'refund': TYPE_REFUND, 40 | 'refund_manual': TYPE_REFUND_MANUAL, 41 | 'reversal': TYPE_REVERSAL, 42 | 'reversal_manual': TYPE_REVERSAL_MANUAL, 43 | } 44 | 45 | TYPE_REFUNDS = (TYPE_REFUND, TYPE_REFUND_MANUAL) 46 | TYPE_REVERSALS = (TYPE_REVERSAL, TYPE_REVERSAL_MANUAL) 47 | TYPE_REFUNDS_REVERSALS = TYPE_REFUNDS + TYPE_REVERSALS 48 | 49 | PROVIDER_BANGO = 1 50 | PROVIDER_REFERENCE = 2 51 | PROVIDER_BRAINTREE = 4 52 | 53 | PROVIDERS = { 54 | 'bango': PROVIDER_BANGO, 55 | 'reference': PROVIDER_REFERENCE, 56 | 'braintree': PROVIDER_BRAINTREE 57 | } 58 | 59 | LOG_STATS = 0 60 | LOG_REVENUE = 1 61 | 62 | LOGS = { 63 | 'stats': LOG_STATS, 64 | 'revenue': LOG_REVENUE 65 | } 66 | 67 | STATUSES_CHOICES = invert(STATUSES) 68 | TYPES_CHOICES = invert(TYPES) 69 | LOG_CHOICES = invert(LOGS) 70 | PROVIDERS_CHOICES = invert(PROVIDERS) 71 | -------------------------------------------------------------------------------- /lib/transactions/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/management/__init__.py -------------------------------------------------------------------------------- /lib/transactions/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/management/commands/__init__.py -------------------------------------------------------------------------------- /lib/transactions/serializers.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.core.urlresolvers import reverse 4 | 5 | from rest_framework import serializers 6 | 7 | from lib.transactions.models import Transaction 8 | from solitude.base import BaseSerializer 9 | from solitude.related_fields import PathRelatedField 10 | 11 | 12 | class TransactionSerializer(BaseSerializer): 13 | buyer = PathRelatedField(view_name='generic:buyer-detail', required=False) 14 | seller = PathRelatedField(view_name='generic:seller-detail', 15 | required=False) 16 | seller_product = PathRelatedField( 17 | view_name='generic:sellerproduct-detail', required=False) 18 | related = PathRelatedField( 19 | view_name='generic:transaction-detail', required=False) 20 | uuid = serializers.CharField(required=False) 21 | 22 | class Meta: 23 | model = Transaction 24 | fields = [ 25 | 'amount', 'buyer', 'carrier', 'created', 'currency', 'notes', 26 | 'pay_url', 'provider', 'region', 'related', 'relations', 27 | 'resource_pk', 'resource_uri', 'seller', 28 | 'seller_product', 'source', 'status', 'status_reason', 'type', 29 | 'uid_pay', 'uid_support', 'uuid' 30 | ] 31 | 32 | def transform_relations(self, obj, value): 33 | objs = [] 34 | if obj: 35 | relations = Transaction.objects.filter(related=obj) 36 | for relation in relations: 37 | # Note that if this relation has more relations, it will fail 38 | # to serialize on the recursiveness. This can be fixed in 39 | # DRF 3.x with recursivefield (if we want to). 40 | objs.append(TransactionSerializer(relation).data) 41 | 42 | return objs 43 | 44 | def resource_uri(self, pk): 45 | return reverse('generic:transaction-detail', kwargs={'pk': pk}) 46 | 47 | def validate_uuid(self, attrs, source): 48 | # Provide a default uuid. 49 | if not attrs.get('uuid') and not self.object: 50 | attrs['uuid'] = 'solitude:' + str(uuid.uuid4()) 51 | return attrs 52 | -------------------------------------------------------------------------------- /lib/transactions/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/lib/transactions/tests/__init__.py -------------------------------------------------------------------------------- /lib/transactions/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from tempfile import NamedTemporaryFile 3 | 4 | from django import test 5 | 6 | from nose.tools import eq_, raises 7 | 8 | from lib.sellers.models import Seller, SellerProduct 9 | from lib.transactions import constants 10 | from lib.transactions.management.commands.log import generate_log 11 | from lib.transactions.models import Transaction 12 | 13 | 14 | class TestLog(test.TestCase): 15 | 16 | def setUp(self): 17 | self.name = NamedTemporaryFile().name 18 | seller = Seller.objects.create(uuid='uuid') 19 | self.product = SellerProduct.objects.create(seller=seller, 20 | external_id='xyz') 21 | self.first = Transaction.objects.create( 22 | provider=1, 23 | seller_product=self.product, uuid='uuid') 24 | self.date = self.first.modified.date() 25 | 26 | def results(self): 27 | return csv.reader(open(self.name, 'rb')) 28 | 29 | def test_filter(self): 30 | generate_log(self.date, self.name, 'stats') 31 | output = self.results() 32 | eq_(next(output)[0], 'version') 33 | eq_(next(output)[1], 'uuid') 34 | 35 | @raises(StopIteration) 36 | def test_stats_log_stops(self): 37 | generate_log(self.date, self.name, 'revenue') 38 | output = self.results() 39 | eq_(next(output)[0], 'version') 40 | next(output) # There is no line 1, transaction not written. 41 | 42 | def test_stats_log(self): 43 | self.first.status = constants.STATUS_CHECKED 44 | self.first.save() 45 | 46 | generate_log(self.date, self.name, 'revenue') 47 | output = self.results() 48 | eq_(next(output)[0], 'version') 49 | eq_(next(output)[1], 'uuid') 50 | 51 | @raises(StopIteration) 52 | def test_multiple(self): 53 | self.first.status = constants.STATUS_CHECKED 54 | self.first.log.create(type=constants.LOG_REVENUE) 55 | self.first.save() 56 | 57 | generate_log(self.date, self.name, 'revenue') 58 | output = self.results() 59 | eq_(next(output)[0], 'version') 60 | next(output) # There is no line 1, transaction not written. 61 | 62 | def test_other(self): 63 | self.first.status = constants.STATUS_CHECKED 64 | self.first.log.create(type=constants.LOG_STATS) 65 | self.first.save() 66 | 67 | generate_log(self.date, self.name, 'revenue') 68 | output = self.results() 69 | eq_(next(output)[0], 'version') 70 | eq_(next(output)[1], 'uuid') 71 | 72 | def test_no_seller(self): 73 | self.first.seller_product = None 74 | self.first.save() 75 | generate_log(self.date, self.name, 'stats') 76 | -------------------------------------------------------------------------------- /lib/transactions/urls.py: -------------------------------------------------------------------------------- 1 | from rest_framework.routers import DefaultRouter 2 | 3 | from lib.transactions import views 4 | 5 | router = DefaultRouter() 6 | router.register(r'transaction', views.TransactionViewSet) 7 | 8 | urlpatterns = router.urls 9 | -------------------------------------------------------------------------------- /lib/transactions/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | 3 | from lib.transactions.forms import UpdateForm 4 | from lib.transactions.models import Transaction 5 | from lib.transactions.serializers import TransactionSerializer 6 | from solitude.base import NonDeleteModelViewSet 7 | 8 | 9 | class TransactionViewSet(NonDeleteModelViewSet): 10 | queryset = Transaction.objects.all() 11 | serializer_class = TransactionSerializer 12 | filter_fields = ('uuid', 'seller', 'provider') 13 | 14 | def update(self, request, *args, **kwargs): 15 | # Disallow PUT, but allow PATCH. 16 | if not kwargs.get('partial', False): 17 | return Response(status=405) 18 | 19 | # We only allow very limited transaction changes. 20 | self.object = self.get_object_or_none() 21 | form = UpdateForm( 22 | request.DATA, original_data=self.object.to_dict(), request=request) 23 | if form.is_valid(): 24 | return ( 25 | super(TransactionViewSet, self) 26 | .update(request, *args, **kwargs) 27 | ) 28 | 29 | return self.form_errors(form) 30 | -------------------------------------------------------------------------------- /logs/.keep: -------------------------------------------------------------------------------- 1 | Docker logs will be written to this directory. 2 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Edit this if necessary or override the variable in your environment. 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'solitude.settings') 7 | if len(sys.argv) > 1 and sys.argv[1] == 'test': 8 | os.environ['DJANGO_SETTINGS_MODULE'] = 'solitude.settings.test' 9 | 10 | 11 | # Specifically importing once the environment has been setup. 12 | from django.conf import settings 13 | newrelic_ini = getattr(settings, 'NEWRELIC_INI', None) 14 | 15 | if newrelic_ini and os.path.exists(newrelic_ini): 16 | import newrelic.agent 17 | try: 18 | newrelic.agent.initialize(newrelic_ini) 19 | except newrelic.api.exceptions.ConfigurationError: 20 | import logging 21 | startup_logger = logging.getLogger('s.startup') 22 | startup_logger.exception('Failed to load new relic config.') 23 | 24 | from solitude.utils import validate_settings 25 | validate_settings() 26 | 27 | 28 | # Alter solitude to run on a particular port as per the 29 | # marketplace docs, unless overridden. 30 | from django.core.management.commands import runserver 31 | runserver.DEFAULT_PORT = 2602 32 | 33 | if __name__ == "__main__": 34 | from django.core.management import execute_from_command_line 35 | execute_from_command_line(sys.argv) 36 | -------------------------------------------------------------------------------- /migrations/01-noop.sql: -------------------------------------------------------------------------------- 1 | -- Example database migration for schematic. Remove this, if you like. 2 | -------------------------------------------------------------------------------- /migrations/02-add-buyers.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | CREATE TABLE `buyer` ( 3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 4 | `uuid` varchar(255) UNIQUE NOT NULL 5 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 6 | 7 | CREATE TABLE `buyer_paypal` ( 8 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 9 | `key` varchar(255), 10 | `expiry` date, 11 | `currency` varchar(3), 12 | `buyer_id` int(11) unsigned NOT NULL UNIQUE 13 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 14 | 15 | ALTER TABLE `buyer_paypal` ADD CONSTRAINT `buyer_id_refs_id_34d8ab16` 16 | FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`); 17 | COMMIT; 18 | -------------------------------------------------------------------------------- /migrations/03-add-sellers.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | CREATE TABLE `seller` ( 3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 4 | `uuid` varchar(255) NOT NULL UNIQUE 5 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 6 | 7 | CREATE TABLE `seller_paypal` ( 8 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 9 | `paypal_id` varchar(255), 10 | `token` varchar(255), 11 | `secret` varchar(255), 12 | `seller_id` int(11) unsigned NOT NULL UNIQUE 13 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 14 | 15 | ALTER TABLE `seller_paypal` ADD CONSTRAINT `seller_id_refs_id_829de2a6` 16 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 17 | COMMIT; 18 | -------------------------------------------------------------------------------- /migrations/04-create-transactions.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | CREATE TABLE `transaction_paypal` ( 3 | `id` int(11) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, 4 | `uuid` varchar(255) NOT NULL, 5 | `seller_id` int(11) UNSIGNED NOT NULL, 6 | `amount` numeric(9, 2) NOT NULL, 7 | `currency` varchar(3) NOT NULL, 8 | `pay_key` varchar(255) NOT NULL, 9 | `correlation_id` varchar(255) NOT NULL, 10 | `type` int(11) UNSIGNED NOT NULL, 11 | `status` int(11) UNSIGNED NOT NULL 12 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 13 | 14 | ALTER TABLE `transaction_paypal` ADD CONSTRAINT `seller_id_refs_id_8e346857` 15 | FOREIGN KEY (`seller_id`) REFERENCES `seller_paypal` (`id`); 16 | CREATE INDEX `transaction_paypal_2bbc74ae` ON `transaction_paypal` (`uuid`); 17 | CREATE INDEX `transaction_paypal_2ef613c9` ON `transaction_paypal` (`seller_id`); 18 | CREATE INDEX `transaction_paypal_278d2c0e` ON `transaction_paypal` (`pay_key`); 19 | CREATE INDEX `transaction_paypal_6fa770c7` ON `transaction_paypal` (`correlation_id`); 20 | COMMIT; 21 | -------------------------------------------------------------------------------- /migrations/05-add-related.sql: -------------------------------------------------------------------------------- 1 | BEGIN; 2 | ALTER TABLE `transaction_paypal` ADD COLUMN `related_id` int(11) unsigned; 3 | ALTER TABLE `transaction_paypal` ADD CONSTRAINT `related_id_refs_id_d6e42cbd` 4 | FOREIGN KEY (`related_id`) REFERENCES `transaction_paypal` (`id`); 5 | CREATE INDEX `transaction_paypal_cb822826` ON `transaction_paypal` (`related_id`); 6 | COMMIT; 7 | -------------------------------------------------------------------------------- /migrations/06-add-sellers-data.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `seller_paypal` 2 | ADD COLUMN `first_name` varchar(255) NOT NULL, 3 | ADD COLUMN `last_name` varchar(255) NOT NULL, 4 | ADD COLUMN `full_name` varchar(255) NOT NULL, 5 | ADD COLUMN `business_name` varchar(255) NOT NULL, 6 | ADD COLUMN `country` varchar(64) NOT NULL, 7 | ADD COLUMN `address_one` varchar(255) NOT NULL, 8 | ADD COLUMN `address_two` varchar(255) NOT NULL, 9 | ADD COLUMN `post_code` varchar(128) NOT NULL, 10 | ADD COLUMN `city` varchar(128) NOT NULL, 11 | ADD COLUMN `state` varchar(64) NOT NULL, 12 | ADD COLUMN `phone` varchar(32) NOT NULL; 13 | -------------------------------------------------------------------------------- /migrations/07-add-sellers-bluevia.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `seller_bluevia` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `bluevia_id` varchar(255), 4 | `seller_id` int(11) unsigned NOT NULL UNIQUE 5 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 6 | -------------------------------------------------------------------------------- /migrations/08-add-source-column.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `transaction_paypal` ADD COLUMN `source` varchar(255); 2 | -------------------------------------------------------------------------------- /migrations/09-add-datetime.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE buyer ADD COLUMN created datetime NOT NULL; 2 | ALTER TABLE buyer ADD COLUMN modified datetime NOT NULL; 3 | 4 | ALTER TABLE buyer_paypal ADD COLUMN created datetime NOT NULL; 5 | ALTER TABLE buyer_paypal ADD COLUMN modified datetime NOT NULL; 6 | 7 | ALTER TABLE seller ADD COLUMN created datetime NOT NULL; 8 | ALTER TABLE seller ADD COLUMN modified datetime NOT NULL; 9 | 10 | ALTER TABLE seller_paypal ADD COLUMN created datetime NOT NULL; 11 | ALTER TABLE seller_paypal ADD COLUMN modified datetime NOT NULL; 12 | 13 | ALTER TABLE seller_bluevia ADD COLUMN created datetime NOT NULL; 14 | ALTER TABLE seller_bluevia ADD COLUMN modified datetime NOT NULL; 15 | 16 | ALTER TABLE transaction_paypal ADD COLUMN created datetime NOT NULL; 17 | ALTER TABLE transaction_paypal ADD COLUMN modified datetime NOT NULL; 18 | -------------------------------------------------------------------------------- /migrations/10-increase-columns.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE buyer_paypal MODIFY `key` longtext; 2 | ALTER TABLE seller_paypal MODIFY `token` longtext; 3 | ALTER TABLE seller_paypal MODIFY `paypal_id` longtext; 4 | ALTER TABLE seller_paypal MODIFY `secret` longtext; 5 | ALTER TABLE seller_bluevia MODIFY `bluevia_id` longtext; 6 | -------------------------------------------------------------------------------- /migrations/11-add-product.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE `seller_product` ( 3 | `id` int(11) AUTO_INCREMENT NOT NULL PRIMARY KEY, 4 | `created` datetime NOT NULL, 5 | `modified` datetime NOT NULL, 6 | `seller_id` int(11) unsigned NOT NULL, 7 | `bango_secret` longtext 8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | 10 | ALTER TABLE `seller_product` ADD CONSTRAINT `seller_id_refs_id_product` 11 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 12 | CREATE INDEX `seller_product` ON `seller_product` (`seller_id`); 13 | -------------------------------------------------------------------------------- /migrations/12-add-pin-to-buyers.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `pin` varchar(255); 2 | -------------------------------------------------------------------------------- /migrations/13-rename-secret.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product CHANGE bango_secret secret LONGTEXT; 2 | -------------------------------------------------------------------------------- /migrations/14-add-seller-bango.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `seller_bango` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `seller_id` int(11) unsigned NOT NULL UNIQUE, 6 | `package_id` int(11) NOT NULL UNIQUE, 7 | `admin_person_id` int(11) NOT NULL, 8 | `support_person_id` int(11) NOT NULL, 9 | `finance_person_id` int(11) NOT NULL 10 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 11 | 12 | ALTER TABLE `seller_bango` ADD CONSTRAINT `seller_id_refs_id_c6c7badb` 13 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 14 | -------------------------------------------------------------------------------- /migrations/15-add-product-bango.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product MODIFY id int(11) UNSIGNED AUTO_INCREMENT; 2 | 3 | CREATE TABLE `seller_product_bango` ( 4 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 5 | `created` datetime NOT NULL, 6 | `modified` datetime NOT NULL, 7 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE, 8 | `seller_bango_id` int(11) unsigned NOT NULL UNIQUE, 9 | `bango_id` varchar(50) NOT NULL 10 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 11 | 12 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_product_id_refs_id_bango` 13 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`); 14 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_bango_id_refs_id_bango` 15 | FOREIGN KEY (`seller_bango_id`) REFERENCES `seller_bango` (`id`); 16 | -------------------------------------------------------------------------------- /migrations/16-redo-transactions.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE transaction_paypal; 2 | CREATE TABLE `transaction` ( 3 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 4 | `created` datetime NOT NULL, 5 | `modified` datetime NOT NULL, 6 | `amount` numeric(9, 2) NOT NULL, 7 | `buyer_id` int(11) UNSIGNED, 8 | `currency` varchar(3) NOT NULL, 9 | `provider` int(11) UNSIGNED NOT NULL, 10 | `related_id` int(11) UNSIGNED , 11 | `seller_id` int(11) UNSIGNED NOT NULL, 12 | `status` int(11) UNSIGNED NOT NULL, 13 | `source` varchar(255), 14 | `type` int(11) UNSIGNED NOT NULL, 15 | `uid_support` varchar(255) NOT NULL UNIQUE, 16 | `uid_pay` varchar(255) NOT NULL UNIQUE, 17 | `uuid` varchar(255) NOT NULL UNIQUE 18 | ) 19 | ; 20 | ALTER TABLE `transaction` ADD CONSTRAINT `buyer_id_refs_id_buyer` 21 | FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`); 22 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_id_refs_id_seller` 23 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 24 | ALTER TABLE `transaction` ADD CONSTRAINT `related_id_refs_id_transaction` 25 | FOREIGN KEY (`related_id`) REFERENCES `transaction` (`id`); 26 | CREATE INDEX `transaction_buyer` ON `transaction` (`buyer_id`); 27 | CREATE INDEX `transaction_related` ON `transaction` (`related_id`); 28 | CREATE INDEX `transaction_seller` ON `transaction` (`seller_id`); 29 | CREATE INDEX `transaction_source` ON `transaction` (`source`); 30 | -------------------------------------------------------------------------------- /migrations/17-fix-transactions.sql: -------------------------------------------------------------------------------- 1 | -- Do stuff I forgot to do first time. 2 | ALTER TABLE transaction ENGINE = InnoDB AUTO_INCREMENT = 1 3 | DEFAULT CHARSET = utf8 COLLATE=utf8_unicode_ci; 4 | 5 | -- Make these columns unique when combined with provider. 6 | ALTER TABLE transaction DROP INDEX uid_pay; 7 | ALTER TABLE transaction DROP INDEX uid_support; 8 | ALTER TABLE transaction ADD INDEX (`uid_pay`, `provider`); 9 | ALTER TABLE transaction ADD INDEX (`uid_support`, `provider`); 10 | 11 | -- Make uid_support nullable; 12 | ALTER TABLE transaction modify uid_support varchar(255); 13 | -------------------------------------------------------------------------------- /migrations/19-fix-product-bango.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM seller_product_bango; 2 | ALTER TABLE seller_product_bango ADD COLUMN uuid varchar(255) NOT NULL UNIQUE; 3 | -------------------------------------------------------------------------------- /migrations/20-fix-transactions.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM `transaction`; 2 | ALTER TABLE `transaction` DROP FOREIGN KEY `seller_id_refs_id_seller`; 3 | ALTER TABLE `transaction` DROP COLUMN `seller_id`; 4 | ALTER TABLE `transaction` ADD COLUMN `seller_product_id` int(11) unsigned NOT NULL; 5 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_product_id_refs_id_product` 6 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`); 7 | -------------------------------------------------------------------------------- /migrations/21-fix-seller-product.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product_bango DROP COLUMN uuid; 2 | DELETE FROM seller_product_bango; 3 | DELETE FROM seller_product; 4 | ALTER TABLE seller_product ADD COLUMN external_id varchar(255) NOT NULL; 5 | ALTER TABLE seller_product ADD UNIQUE (`seller_id`, `external_id`); 6 | CREATE INDEX `seller_product_d5e787` ON `seller_product` (`external_id`); 7 | -------------------------------------------------------------------------------- /migrations/22-fix-product-bango.sql: -------------------------------------------------------------------------------- 1 | # seller_bango is many-to-one and should not be unique. 2 | ALTER TABLE seller_product_bango DROP FOREIGN KEY seller_bango_id_refs_id_bango; 3 | ALTER TABLE seller_product_bango DROP INDEX seller_bango_id; 4 | ALTER TABLE `seller_product_bango` ADD CONSTRAINT `seller_bango_id_refs_id_bango` 5 | FOREIGN KEY (`seller_bango_id`) REFERENCES `seller_bango` (`id`); 6 | -------------------------------------------------------------------------------- /migrations/23-add-delayable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `delayable` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `uuid` varchar(36) NOT NULL, 6 | `run` bool NOT NULL, 7 | `status_code` int(11) unsigned NOT NULL, 8 | `content` longtext NOT NULL 9 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 10 | -------------------------------------------------------------------------------- /migrations/24-add-pin-confirmed-to-buyer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `pin_confirmed` boolean NOT NULL DEFAULT 0; 2 | -------------------------------------------------------------------------------- /migrations/25-add-active.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `active` bool NOT NULL DEFAULT True; 2 | CREATE INDEX `buyer_active` ON `buyer` (`active`); 3 | ALTER TABLE `seller` ADD COLUMN `active` bool NOT NULL DEFAULT True; 4 | CREATE INDEX `seller_active` ON `buyer` (`active`); 5 | -------------------------------------------------------------------------------- /migrations/26-amount-optional.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE transaction MODIFY amount decimal(9,2); 2 | -------------------------------------------------------------------------------- /migrations/27-add-notes.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE transaction ADD COLUMN notes longtext; 2 | -------------------------------------------------------------------------------- /migrations/28-add-reset-to-buyer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `new_pin` varchar(255); 2 | ALTER TABLE `buyer` ADD COLUMN `needs_pin_reset` boolean NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /migrations/29-add-pin-locking.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `pin_failures` integer NOT NULL; 2 | ALTER TABLE `buyer` ADD COLUMN `pin_locked_out` datetime; 3 | -------------------------------------------------------------------------------- /migrations/30-add-sbi-expiration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_bango ADD COLUMN sbi_expires datetime; 2 | -------------------------------------------------------------------------------- /migrations/31-add-public-id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product ADD COLUMN public_id varchar(255) NOT NULL; 2 | -------------------------------------------------------------------------------- /migrations/32-populate-public-id.py: -------------------------------------------------------------------------------- 1 | # Removed to be able to run future migrations altering the SellerProduct model. 2 | -------------------------------------------------------------------------------- /migrations/33-add-unique.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product MODIFY public_id varchar(255) NOT NULL UNIQUE; 2 | -------------------------------------------------------------------------------- /migrations/34-seller-product-access.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product ADD COLUMN access int(11) UNSIGNED NOT NULL DEFAULT 1; 2 | ALTER TABLE seller_product ALTER COLUMN access DROP DEFAULT; 3 | -------------------------------------------------------------------------------- /migrations/35-add-pin-was-locked-to-buyer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `pin_was_locked_out` boolean NOT NULL DEFAULT 0; 2 | -------------------------------------------------------------------------------- /migrations/36-add-bango-status.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `status_bango` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `status` int(11) NOT NULL, 6 | `errors` longtext NOT NULL, 7 | `seller_product_bango_id` int(11) unsigned NOT NULL 8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | 10 | ALTER TABLE `status_bango` ADD CONSTRAINT `seller_product_bango_id_refs_id_1c77c54e` FOREIGN KEY (`seller_product_bango_id`) REFERENCES `seller_product_bango` (`id`); 11 | CREATE INDEX `status_bango_eae63669` ON `status_bango` (`seller_product_bango_id`); 12 | -------------------------------------------------------------------------------- /migrations/37-add-counter-field.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 2 | ALTER TABLE `buyer_paypal` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 3 | ALTER TABLE `delayable` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 4 | ALTER TABLE `seller` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 5 | ALTER TABLE `seller_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 6 | ALTER TABLE `seller_paypal` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 7 | ALTER TABLE `seller_product` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 8 | ALTER TABLE `seller_product_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 9 | ALTER TABLE `status_bango` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 10 | ALTER TABLE `transaction` ADD COLUMN `counter` bigint(20) NULL DEFAULT 0; 11 | -------------------------------------------------------------------------------- /migrations/38-alter-uid-pay.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE transaction CHANGE COLUMN uid_pay uid_pay varchar(255); 2 | -------------------------------------------------------------------------------- /migrations/39-add-transaction-log.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `transaction_log` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `transaction_id` int(11) unsigned NOT NULL, 7 | `type` int(11) unsigned NOT NULL 8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | 10 | ALTER TABLE `transaction_log` ADD CONSTRAINT `transaction_id_id` FOREIGN KEY (`transaction_id`) REFERENCES `transaction` (`id`); 11 | -------------------------------------------------------------------------------- /migrations/40-add-region-carrier.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `transaction` ADD COLUMN `carrier` varchar(255); 2 | ALTER TABLE `transaction` ADD COLUMN `region` varchar(255); 3 | -------------------------------------------------------------------------------- /migrations/41-add-seller-boku.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `seller_boku` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `seller_id` int(11) unsigned NOT NULL UNIQUE, 7 | `merchant_id` varchar(255) NOT NULL, 8 | `service_id` varchar(255) NOT NULL 9 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 10 | 11 | ALTER TABLE `seller_boku` ADD CONSTRAINT `seller_id_refs_id_d3a72381` FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 12 | -------------------------------------------------------------------------------- /migrations/42-transaction-pay-url.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `transaction` ADD COLUMN `pay_url` varchar(255) NULL; 2 | -------------------------------------------------------------------------------- /migrations/43-remove-merchant-id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `seller_boku` DROP COLUMN `merchant_id`; 2 | -------------------------------------------------------------------------------- /migrations/44-add-product-boku.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `seller_product_boku` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE, 7 | `seller_boku_id` int(11) unsigned NOT NULL UNIQUE 8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | 10 | ALTER TABLE `seller_product_boku` ADD CONSTRAINT `seller_product_id_refs_id_boku` 11 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`); 12 | ALTER TABLE `seller_product_boku` ADD CONSTRAINT `seller_boku_id_refs_id_boku` 13 | FOREIGN KEY (`seller_boku_id`) REFERENCES `seller_boku` (`id`); 14 | -------------------------------------------------------------------------------- /migrations/45-add-seller-to-transactions.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `transaction` ADD COLUMN `seller_id` int(11) unsigned NULL; 2 | ALTER TABLE `transaction` ADD CONSTRAINT `seller_id_refs_id_seller` 3 | FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 4 | -------------------------------------------------------------------------------- /migrations/46-remove-unique-constraint-for-boku.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `seller_product_boku` 2 | DROP FOREIGN KEY `seller_boku_id_refs_id_boku`, 3 | DROP INDEX `seller_boku_id`; 4 | ALTER TABLE `seller_product_boku` 5 | ADD INDEX `seller_boku_id` (`seller_boku_id`), 6 | ADD CONSTRAINT `seller_boku_id_refs_id_boku` FOREIGN KEY (`seller_boku_id`) REFERENCES `seller_boku` (`id`); 7 | -------------------------------------------------------------------------------- /migrations/47-add-product-reference.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `seller_reference` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `seller_id` int(11) unsigned NOT NULL UNIQUE, 7 | `merchant_id` varchar(255) NOT NULL 8 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 9 | 10 | ALTER TABLE `seller_reference` ADD CONSTRAINT `seller_id_refs_id_reference` FOREIGN KEY (`seller_id`) REFERENCES `seller` (`id`); 11 | 12 | CREATE TABLE `seller_product_reference` ( 13 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 14 | `created` datetime NOT NULL, 15 | `modified` datetime NOT NULL, 16 | `counter` bigint, 17 | `seller_product_id` int(11) unsigned NOT NULL UNIQUE, 18 | `seller_reference_id` int(11) unsigned NOT NULL UNIQUE 19 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 20 | 21 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_product_id_refs_id_reference` 22 | FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`); 23 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_reference_id_refs_id_reference` 24 | FOREIGN KEY (`seller_reference_id`) REFERENCES `seller_reference` (`id`); 25 | -------------------------------------------------------------------------------- /migrations/48-add-email-to-buyer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE buyer ADD COLUMN `email` longtext; 2 | -------------------------------------------------------------------------------- /migrations/49-add-reference-id.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `seller_product_reference` ADD COLUMN `reference_id` VARCHAR(255) NOT NULL; 2 | ALTER TABLE `seller_reference` CHANGE `merchant_id` `reference_id` VARCHAR(255) NOT NULL; 3 | -------------------------------------------------------------------------------- /migrations/50-fix-seller-product-reference.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE seller_product_reference DROP FOREIGN KEY seller_reference_id_refs_id_reference; 2 | # Remove the unique index. 3 | DROP INDEX seller_reference_id ON seller_product_reference; 4 | ALTER TABLE `seller_product_reference` ADD CONSTRAINT `seller_reference_id_refs_id_reference` 5 | FOREIGN KEY (`seller_reference_id`) REFERENCES `seller_reference` (`id`); 6 | -------------------------------------------------------------------------------- /migrations/51-make-provider-optional.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE transaction CHANGE COLUMN `seller_product_id` `seller_product_id` int(11) unsigned DEFAULT NULL; 2 | ALTER TABLE transaction CHANGE COLUMN `provider` `provider` int(11) unsigned DEFAULT NULL; 3 | -------------------------------------------------------------------------------- /migrations/52-add-error-reason.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE transaction ADD COLUMN status_reason varchar(255) DEFAULT NULL; 2 | -------------------------------------------------------------------------------- /migrations/53-remove-paypal.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE seller_paypal; 2 | -------------------------------------------------------------------------------- /migrations/54-add-braintree-buyer.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `buyer_braintree` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `active` bool NOT NULL, 7 | `braintree_id` varchar(255) NOT NULL UNIQUE, 8 | `buyer_id` int(11) UNSIGNED NOT NULL UNIQUE 9 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 10 | ALTER TABLE `buyer_braintree` ADD CONSTRAINT `buyer_id` FOREIGN KEY (`buyer_id`) REFERENCES `buyer` (`id`); 11 | -------------------------------------------------------------------------------- /migrations/55-add-payment-method.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `braintree_pay_method` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `active` bool NOT NULL, 7 | `braintree_buyer_id` int(11) UNSIGNED NOT NULL, 8 | `provider_id` varchar(255) NOT NULL, 9 | `type` integer UNSIGNED NOT NULL, 10 | `type_name` varchar(255) NOT NULL, 11 | `truncated_id` varchar(255) NOT NULL 12 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 13 | ALTER TABLE `braintree_pay_method` ADD CONSTRAINT `braintree_buyer_id_refs_id_f404a9d7` FOREIGN KEY (`braintree_buyer_id`) REFERENCES `buyer_braintree` (`id`); 14 | CREATE INDEX `braintree_pay_method_ab6bc121` ON `braintree_pay_method` (`braintree_buyer_id`); 15 | -------------------------------------------------------------------------------- /migrations/56-add-subscriptions.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `braintree_subscription` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime NOT NULL, 4 | `modified` datetime NOT NULL, 5 | `counter` bigint, 6 | `active` bool NOT NULL, 7 | `paymethod_id` int(11) unsigned NOT NULL, 8 | `seller_product_id` int(11) unsigned NOT NULL, 9 | `provider_id` varchar(255) NOT NULL, 10 | UNIQUE (`paymethod_id`, `seller_product_id`) 11 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 12 | ALTER TABLE `braintree_subscription` ADD CONSTRAINT `seller_product_id_refs_id_b5dd89a4` FOREIGN KEY (`seller_product_id`) REFERENCES `seller_product` (`id`); 13 | ALTER TABLE `braintree_subscription` ADD CONSTRAINT `paymethod_id_refs_id_1812d9f7` FOREIGN KEY (`paymethod_id`) REFERENCES `braintree_pay_method` (`id`); 14 | CREATE INDEX `braintree_subscription_2bb89b7c` ON `braintree_subscription` (`paymethod_id`); 15 | CREATE INDEX `braintree_subscription_d18be639` ON `braintree_subscription` (`seller_product_id`); 16 | -------------------------------------------------------------------------------- /migrations/57-add-braintree-transaction.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `braintree_transaction` ( 2 | `id` int(11) unsigned AUTO_INCREMENT NOT NULL PRIMARY KEY, 3 | `created` datetime(6) NOT NULL, 4 | `modified` datetime(6) NOT NULL, 5 | `counter` bigint, 6 | `transaction_id` int(11) unsigned NOT NULL UNIQUE, 7 | `paymethod_id` int(11) unsigned NOT NULL, 8 | `subscription_id` int(11) unsigned NOT NULL, 9 | `billing_period_end_date` datetime(6) NOT NULL, 10 | `billing_period_start_date` datetime(6) NOT NULL, 11 | `kind` varchar(255) NOT NULL, 12 | `next_billing_date` datetime(6) NOT NULL, 13 | `next_billing_period_amount` numeric(9, 2) 14 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; 15 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `subscription_id_refs_id_76dfe860` FOREIGN KEY (`subscription_id`) REFERENCES `braintree_subscription` (`id`); 16 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `paymethod_id_refs_id_e9b15dff` FOREIGN KEY (`paymethod_id`) REFERENCES `braintree_pay_method` (`id`); 17 | ALTER TABLE `braintree_transaction` ADD CONSTRAINT `transaction_id_refs_id_35773a94` FOREIGN KEY (`transaction_id`) REFERENCES `transaction` (`id`); 18 | CREATE INDEX `braintree_transaction_2bb89b7c` ON `braintree_transaction` (`paymethod_id`); 19 | CREATE INDEX `braintree_transaction_b75baf19` ON `braintree_transaction` (`subscription_id`); 20 | -------------------------------------------------------------------------------- /migrations/58-add-locale.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `locale` VARCHAR(255); 2 | -------------------------------------------------------------------------------- /migrations/59-buyer-optional.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `paymethod_id` `paymethod_id` int(11) unsigned; 2 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `subscription_id` `subscription` int(11) unsigned; 3 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `billing_period_end_date` `billing_period_end_date` datetime(6); 4 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `billing_period_start_date` `billing_period_start_date` datetime(6); 5 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `next_billing_date` `next_billing_date` datetime(6); 6 | -------------------------------------------------------------------------------- /migrations/60-fix-subscription.sql: -------------------------------------------------------------------------------- 1 | # This was accidentally changed in https://github.com/mozilla/solitude/pull/535 2 | # This patch puts it back. 3 | ALTER TABLE `braintree_transaction` CHANGE COLUMN `subscription` `subscription_id` int(11) unsigned; 4 | -------------------------------------------------------------------------------- /migrations/61-bt-subsription-amount.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE braintree_subscription ADD COLUMN `amount` numeric(9, 2); 2 | -------------------------------------------------------------------------------- /migrations/62-authenticated-buyer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `authenticated` boolean NOT NULL DEFAULT 1; 2 | -------------------------------------------------------------------------------- /migrations/63-buyer-email-sig.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `buyer` ADD COLUMN `email_sig` varchar(255); 2 | -------------------------------------------------------------------------------- /migrations/64-populate-email-hash.py: -------------------------------------------------------------------------------- 1 | from lib.buyers.models import Buyer 2 | from solitude.logger import getLogger 3 | 4 | log = getLogger(__name__) 5 | 6 | 7 | def run(): 8 | for buyer in Buyer.objects.values('id', 'email'): 9 | if not buyer['email']: 10 | continue 11 | 12 | log.info('Updating email_sig for {}'.format(buyer['id'])) 13 | Buyer.objects.get(pk=buyer['id']).save() 14 | -------------------------------------------------------------------------------- /migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/migrations/__init__.py -------------------------------------------------------------------------------- /migrations/schematic_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 5 | 6 | # Set up playdoh. 7 | import manage # noqa 8 | from django.conf import settings 9 | 10 | config = settings.DATABASES['default'] 11 | config['HOST'] = config.get('HOST', 'localhost') 12 | config['PORT'] = config.get('PORT', '3306') 13 | 14 | if not config['HOST'] or config['HOST'].endswith('.sock'): 15 | """Oh, you meant 'localhost'!""" 16 | config['HOST'] = 'localhost' 17 | 18 | s = 'mysql --silent {NAME} -h{HOST} -u{USER}' 19 | 20 | if config['PASSWORD']: 21 | os.environ['MYSQL_PWD'] = config['PASSWORD'] 22 | del config['PASSWORD'] 23 | 24 | if config['PORT']: 25 | s += ' -P{PORT}' 26 | else: 27 | del config['PORT'] 28 | 29 | db = s.format(**config) 30 | table = 'schema_version' 31 | handlers = {'.py': sys.executable + ' -B manage.py runscript migrations.%s'} 32 | -------------------------------------------------------------------------------- /requirements/compiled.txt: -------------------------------------------------------------------------------- 1 | # pyrepo (for docker and fabfile) 2 | # sha256: GgH9vZMrh5fOLSq3tfsLLLsLZqizC1qbEOPiOzhd4GM 3 | # pyrepo/travis (for travis) 4 | # sha256: eSnjUWPzFnUf4tM90UBXKeP6cJ-U_BopmJ7-gnFwXzk 5 | M2Crypto==0.22.3 6 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # This file pulls in everything a developer needs. If it's a basic package 2 | # needed to run the site, it belongs in requirements/prod.txt. If it's a 3 | # package for developers (testing, docs, etc.), it goes in this file. 4 | 5 | -r test.txt 6 | 7 | # sha256: bpQ8fq1eguaZWQ5Uh551R1RA4bTk2gfsHkyiNCdT4bw 8 | autopep8==1.1.1 9 | 10 | # sha256: d8kqqvXXiwo6jDT-0cYikggqtS4mHE831h2S4MO3vy8 11 | baked==0.2.5 12 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # For buildings docs (also used by RTD) 2 | 3 | # sha256: BwPB6lpq8LttDOwkcIMBM0lJ1W68f5XGQCjZxm-djV0 4 | alabaster==0.7.3 5 | 6 | # sha256: nwLQNXGE3h8JPBABK1LnRUoQCL5qXBhat6Mwes6x0S4 7 | babel==1.3 8 | 9 | # sha256: rkt0OyT9AsmZlDoV8BSzl2KoeKmdk38QK_aBpINhP6M 10 | docutils==0.12 11 | 12 | # sha256: LiSsXQBNtXFJdqBKwOgMbfbkfpjDVMssDYL4h51Pj9s 13 | Jinja2==2.7.3 14 | 15 | # sha256: RDoLYzhx6Cc4e8xA4mqZdKbsmJHjZWRUOPBsszhuv2Q 16 | MarkupSafe==0.23 17 | 18 | # sha256: CjoiZeHvsN76ci0fQpcw3J35da3DxgrH0rQyvfA8BB0 19 | Pygments==2.0.2 20 | 21 | # sha256: NYsFoMJgXBXPgzfRcBbrla_qrQfJAN-cLN524hlKklg 22 | pytz==2015.2 23 | 24 | # sha256: QYqTw5en7asj5ViNvAZ6x0pyPts9VBvUk295R252Rdo 25 | six==1.9.0 26 | 27 | # sha256: FcW-zVBL1OnVt8BieEaXBrtZ343SoriwJ4D0wUgL7_4 28 | snowballstemmer==1.1.0 29 | 30 | # sha256: Ld8Y2jsGIfpD_uS3KQ2grnibRvuJkniorMzaGVxJeac 31 | Sphinx==1.3.1 32 | 33 | # sha256: qKLiOalzG2hcfepiSF3sgc7zYB6-UKd6lalr3yl6-_c 34 | sphinx-rtd-theme==0.1.7 35 | 36 | # sha256: alM5uT6s8XI2Wnzex-CtWm5YNJ_BNu9umUx0nKXGG6c 37 | sphinxcontrib-httpdomain==1.3.0 38 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | # This file pulls in everything Jenkins needs for CI. If it's a basic package 2 | # needed to run the site, it belongs in requirements/prod.txt. If it's a 3 | # package for developers (testing, docs, etc.), it goes in dev.txt. 4 | # If it's something we only need on Jenkins it goes here. 5 | -r prod.txt 6 | 7 | # Flake8 requirements 8 | # sha256: yZzJcW1mVdnIvLHndjK4YVvwq9KC16vZ9cIUjK1_xmk 9 | flake8==2.3.0 10 | 11 | # sha256: rEVxaVwQzhU2vNuhopS58tPmzJ0OoXG2fVCghkzj4EI 12 | pyflakes==0.8.1 13 | 14 | # sha256: Yuh_1UU1-5MrSk2Uho21IyV6EDHIvGvTWMIBVDPmRts 15 | pep8==1.5.7 16 | 17 | # sha256: ic9r9P9kT_i5EuL6fiq7aNV6jn6Rjjk8coQNMNLrzws 18 | mccabe==0.3 19 | 20 | # sha256: Utw-E4etWsB_KruzYtWovPUmUW7UoFF6ay6EGr2O6rw 21 | # sha256: ku47hrXBkhwtnawhe--JkMX4Usi8TZAYyv4PPzuBzTo 22 | nosenicedots==0.5 23 | 24 | # sha256: Zf-5k0lWWuoQyYANEgM4TUhuwUp8gz31dVdsRfTp8Dk 25 | # sha256: c0XZOWs_PApljFy328eZw6lq-kgpnZbZeB6rprqwt7A 26 | nose-blockage==0.1.2 27 | -------------------------------------------------------------------------------- /samples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/samples/__init__.py -------------------------------------------------------------------------------- /samples/lib.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | import sys 4 | 5 | import requests 6 | 7 | 8 | # TODO: rewrite this to use curling? 9 | def call(root, url, method, data): 10 | method = getattr(requests, method) 11 | url = root + url 12 | print method.__name__.upper(), url 13 | print 'Request data:' 14 | pprint.pprint(data) 15 | data = json.dumps(data) 16 | result = method(url, data=data, 17 | headers={ 18 | 'content-type': 'application/json', 19 | 'accept': 'application/json', 20 | }) 21 | print 'Status code:', result.status_code 22 | if result.status_code not in (200, 201, 202, 204): 23 | print 'Error:' 24 | try: 25 | print pprint.pprint(json.loads(result.content)) 26 | except ValueError: 27 | print result.content 28 | sys.exit() 29 | 30 | if result.content: 31 | print 'Response data:' 32 | data = result.json() if callable(result.json) else result.json 33 | pprint.pprint(data) 34 | print 35 | return data 36 | 37 | print 38 | -------------------------------------------------------------------------------- /samples/readme.txt: -------------------------------------------------------------------------------- 1 | A list of samples of doing certain tasks. 2 | -------------------------------------------------------------------------------- /solitude/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/__init__.py -------------------------------------------------------------------------------- /solitude/constants.py: -------------------------------------------------------------------------------- 1 | # From zamboni, the different payment types that zamboni knows about. 2 | PAYMENT_METHOD_OPERATOR = 0 3 | PAYMENT_METHOD_CARD = 1 4 | PAYMENT_METHOD_ALL = 2 5 | 6 | PAYMENT_METHOD_CHOICES = ( 7 | PAYMENT_METHOD_OPERATOR, 8 | PAYMENT_METHOD_CARD, 9 | PAYMENT_METHOD_ALL 10 | ) 11 | 12 | # Not including "all" which is really "both" because for Payment Methods that 13 | # doesn't make sense. 14 | SINGLE_PAYMENT_METHOD = ( 15 | (PAYMENT_METHOD_OPERATOR, PAYMENT_METHOD_OPERATOR), 16 | (PAYMENT_METHOD_OPERATOR, PAYMENT_METHOD_CARD) 17 | ) 18 | -------------------------------------------------------------------------------- /solitude/errors.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from rest_framework.exceptions import ParseError 4 | 5 | 6 | class ErrorFormatter(object): 7 | 8 | def __init__(self, error): 9 | self.error = error 10 | 11 | 12 | class MozillaFormatter(ErrorFormatter): 13 | 14 | def format(self): 15 | errors = defaultdict(list) 16 | for k, error in self.error.detail.as_data().items(): 17 | for v in error: 18 | errors[k].append({ 19 | 'code': v.code, 20 | 'message': unicode(v.message) 21 | }) 22 | return {'mozilla': dict(errors)} 23 | 24 | 25 | class FormError(ParseError): 26 | status_code = 422 27 | default_detail = 'Error parsing form.' 28 | formatter = MozillaFormatter 29 | 30 | 31 | class InvalidQueryParams(ParseError): 32 | status_code = 400 33 | default_detail = 'Incorrect query parameters.' 34 | -------------------------------------------------------------------------------- /solitude/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db.transaction import set_rollback 3 | 4 | from rest_framework.response import Response 5 | from rest_framework.views import exception_handler 6 | 7 | from lib.bango.errors import BangoImmediateError 8 | from solitude.logger import getLogger 9 | 10 | log = getLogger('s') 11 | 12 | 13 | def custom_exception_handler(exc): 14 | # If you raise an error in solitude, it comes to here and 15 | # we rollback the transaction. 16 | log.info('Handling exception, about to roll back for: {}, {}' 17 | .format(type(exc), exc.message)) 18 | set_rollback(True) 19 | 20 | if hasattr(exc, 'formatter'): 21 | try: 22 | return Response(exc.formatter(exc).format(), 23 | status=getattr(exc, 'status_code', 422)) 24 | except: 25 | # If the formatter fails, fall back to the standard 26 | # error formatting. 27 | log.exception('Failed to use formatter.') 28 | 29 | if isinstance(exc, BangoImmediateError): 30 | return Response(exc.message, status=400) 31 | 32 | return exception_handler(exc) 33 | -------------------------------------------------------------------------------- /solitude/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class ListField(forms.CharField): 5 | 6 | def clean(self, value): 7 | if not isinstance(value, list): 8 | raise forms.ValidationError('Invalid list.') 9 | return value 10 | -------------------------------------------------------------------------------- /solitude/filter.py: -------------------------------------------------------------------------------- 1 | from rest_framework.filters import DjangoFilterBackend 2 | 3 | from solitude.errors import InvalidQueryParams 4 | from solitude.logger import getLogger 5 | 6 | log = getLogger('s.filter') 7 | 8 | 9 | class StrictQueryFilter(DjangoFilterBackend): 10 | 11 | """ 12 | Don't allow people to typo request params and return all the objects. 13 | Instead limit it down to the parameters allowed in filter_fields. 14 | """ 15 | 16 | def get_filter_class(self, view, queryset=None): 17 | klass = (super(StrictQueryFilter, self) 18 | .get_filter_class(view, queryset=queryset)) 19 | try: 20 | # If an ordering exists on the model, use that. 21 | klass._meta.order_by = klass.Meta.model.Meta.ordering 22 | except AttributeError: 23 | pass 24 | return klass 25 | 26 | def filter_queryset(self, request, queryset, view): 27 | requested = set(request.QUERY_PARAMS.keys()) 28 | allowed = set(getattr(view, 'filter_fields', [])) 29 | difference = requested.difference(allowed) 30 | if difference: 31 | raise InvalidQueryParams( 32 | detail='Incorrect query parameters: ' + ','.join(difference)) 33 | 34 | return (super(StrictQueryFilter, self) 35 | .filter_queryset(request, queryset, view)) 36 | -------------------------------------------------------------------------------- /solitude/locale/en_US/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2011-05-26 18:11-0700\n" 6 | "PO-Revision-Date: 2011-05-26 18:11-0700\n" 7 | "Last-Translator: Automatically generated\n" 8 | "Language-Team: none\n" 9 | "Language: en_US\n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=UTF-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Translate Toolkit 1.8.0\n" 14 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 15 | 16 | #: apps/examples/templates/examples/home.html:5 17 | msgid "Hello world" 18 | msgstr "Hello world" 19 | 20 | #. This is a localizer comment 21 | #: apps/examples/templates/examples/home.html:9 22 | msgid "This is a test view." 23 | msgstr "This is a test view." 24 | 25 | #: apps/examples/templates/examples/home.html:11 26 | msgid "Learn you some Playdoh and then go build something awesome." 27 | msgstr "Learn you some Playdoh and then go build something awesome." 28 | 29 | #: apps/examples/templates/examples/home.html:17 30 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s." 31 | msgstr "Current locale: %(LANG)s.
Available locales: %(langs)s." 32 | -------------------------------------------------------------------------------- /solitude/locale/fr/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Project-Id-Version: PACKAGE VERSION\n" 4 | "Report-Msgid-Bugs-To: \n" 5 | "POT-Creation-Date: 2011-06-03 19:07-0700\n" 6 | "Last-Translator: Automatically generated\n" 7 | "Language-Team: none\n" 8 | "Language: fr\n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 13 | 14 | #: apps/examples/templates/examples/home.html:5 15 | msgid "Hello world" 16 | msgstr "Bonjour le monde" 17 | 18 | #. This is a localizer comment 19 | #: apps/examples/templates/examples/home.html:9 20 | msgid "This is a test view." 21 | msgstr "Ceci est une vue de test." 22 | 23 | #: apps/examples/templates/examples/home.html:11 24 | msgid "Learn you some Playdoh and then go build something awesome." 25 | msgstr "Apprends à jouer avec Playdoh et construis quelque chose de génial." 26 | 27 | #: apps/examples/templates/examples/home.html:17 28 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s." 29 | msgstr "Langue active : %(LANG)s.
Langues disponibles : %(langs)s." 30 | -------------------------------------------------------------------------------- /solitude/locale/templates/LC_MESSAGES/messages.pot: -------------------------------------------------------------------------------- 1 | #, fuzzy 2 | msgid "" 3 | msgstr "" 4 | "Project-Id-Version: PACKAGE VERSION\n" 5 | "Report-Msgid-Bugs-To: \n" 6 | "POT-Creation-Date: 2011-06-03 19:07-0700\n" 7 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 8 | "Last-Translator: FULL NAME \n" 9 | "Language-Team: LANGUAGE \n" 10 | "MIME-Version: 1.0\n" 11 | "Content-Type: text/plain; charset=utf-8\n" 12 | "Content-Transfer-Encoding: 8bit\n" 13 | "X-Generator: Translate Toolkit 1.8.0\n" 14 | 15 | #: apps/examples/templates/examples/home.html:5 16 | msgid "Hello world" 17 | msgstr "" 18 | 19 | #. This is a localizer comment 20 | #: apps/examples/templates/examples/home.html:9 21 | msgid "This is a test view." 22 | msgstr "" 23 | 24 | #: apps/examples/templates/examples/home.html:11 25 | msgid "" 26 | "Learn you some Playdoh and then go build " 27 | "something awesome." 28 | msgstr "" 29 | 30 | #: apps/examples/templates/examples/home.html:17 31 | msgid "Current locale: %(LANG)s.
Available locales: %(langs)s." 32 | msgstr "" 33 | -------------------------------------------------------------------------------- /solitude/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | from solitude.middleware import get_oauth_key, get_transaction_id 6 | 7 | 8 | def getLogger(name=None): 9 | logger = logging.getLogger(name) 10 | return SolitudeAdapter(logger) 11 | 12 | 13 | # This really should be fulfilled by a logging filter which would remove the 14 | # need to do all this crap. However I've got no idea how to do that and I 15 | # wasted far too long on this. 16 | class SolitudeAdapter(logging.LoggerAdapter): 17 | 18 | """Adds OAuth user and transaction id to every logging message's kwargs.""" 19 | 20 | def __init__(self, logger, extra=None): 21 | logging.LoggerAdapter.__init__(self, logger, extra or {}) 22 | 23 | def process(self, msg, kwargs): 24 | kwargs['extra'] = {'OAUTH_KEY': get_oauth_key(), 25 | 'TRANSACTION_ID': get_transaction_id()} 26 | return msg, kwargs 27 | 28 | 29 | class SolitudeFormatter(logging.Formatter): 30 | 31 | def format(self, record): 32 | tag = '%s: ' % settings.SYSLOG_TAG 33 | if not self._fmt.startswith(tag): 34 | self._fmt = '%s %s' % (tag, self._fmt) 35 | for name in 'OAUTH_KEY', 'TRANSACTION_ID': 36 | record.__dict__.setdefault(name, '') 37 | return logging.Formatter.format(self, record) 38 | -------------------------------------------------------------------------------- /solitude/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/management/__init__.py -------------------------------------------------------------------------------- /solitude/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/management/commands/__init__.py -------------------------------------------------------------------------------- /solitude/management/commands/push_s3.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from optparse import make_option 4 | 5 | from django.conf import settings 6 | from django.core.management.base import BaseCommand 7 | 8 | import boto 9 | from boto.s3.key import Key 10 | from solitude.logger import getLogger 11 | 12 | log = getLogger('s.s3') 13 | 14 | 15 | def push(source): 16 | if not all(settings.S3_AUTH.values() + [settings.S3_BUCKET, ]): 17 | print 'Settings incomplete, cannot push to S3.' 18 | sys.exit(1) 19 | 20 | dest = os.path.basename(source) 21 | conn = boto.connect_s3(settings.S3_AUTH['key'], 22 | settings.S3_AUTH['secret']) 23 | bucket = conn.get_bucket(settings.S3_BUCKET) 24 | k = Key(bucket) 25 | k.key = dest 26 | k.set_contents_from_filename(source) 27 | log.debug('Uploaded: {0} to: {1}'.format(source, dest)) 28 | 29 | 30 | class Command(BaseCommand): 31 | help = 'Push a file to S3' 32 | option_list = BaseCommand.option_list + ( 33 | make_option('--file', action='store', type='string', dest='file', 34 | default=''), 35 | ) 36 | 37 | def handle(self, *args, **options): 38 | source = options['file'] 39 | if not source or not os.path.exists(source): 40 | print 'File not found: {0}'.format(source) 41 | sys.exit(1) 42 | 43 | push(source) 44 | -------------------------------------------------------------------------------- /solitude/management/commands/refresh_wsdl.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand 5 | 6 | import requests 7 | 8 | from lib.bango.constants import WSDL_MAP 9 | 10 | root = os.path.join(settings.ROOT, 'lib', 'bango', 'wsdl') 11 | 12 | 13 | class Command(BaseCommand): 14 | help = "Refresh the WSDLs." 15 | 16 | def handle(self, *args, **kw): 17 | for dir, wsdls in WSDL_MAP.items(): 18 | for wsdl in wsdls.values(): 19 | filename = wsdl['file'] 20 | src = wsdl['url'] 21 | dest = os.path.join(root, dir, filename) 22 | top = os.path.dirname(dest) 23 | if not os.path.exists(top): 24 | os.makedirs(top) 25 | 26 | print 'Getting', src 27 | response = requests.get(src, verify=False) 28 | if response.status_code != 200: 29 | print ('...returned a {0}, skipping' 30 | .format(response.status_code)) 31 | continue 32 | 33 | open(dest, 'w').write(response.text) 34 | print '...written to', dest 35 | -------------------------------------------------------------------------------- /solitude/middleware.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | _local = threading.local() 4 | 5 | 6 | def get_oauth_key(): 7 | return getattr(_local, 'OAUTH_KEY', '') 8 | 9 | 10 | def get_transaction_id(): 11 | return getattr(_local, 'TRANSACTION_ID', None) 12 | 13 | 14 | def set_oauth_key(key): 15 | _local.OAUTH_KEY = key 16 | 17 | 18 | class LoggerMiddleware(object): 19 | 20 | def process_request(self, request): 21 | _local.TRANSACTION_ID = request.META.get('HTTP_TRANSACTION_ID', '-') 22 | # At the beginning of the request we won't have done authentication 23 | # yet so this sets the value to anon. When authentication is completed 24 | # that will update the oauth_key with the authenticated value. 25 | set_oauth_key(getattr(request, 'OAUTH_KEY', '')) 26 | -------------------------------------------------------------------------------- /solitude/paginator.py: -------------------------------------------------------------------------------- 1 | from rest_framework import pagination 2 | from rest_framework import serializers 3 | from rest_framework.templatetags.rest_framework import replace_query_param 4 | 5 | 6 | class NextPageField(serializers.Field): 7 | 8 | """Wrapper to remove absolute_uri.""" 9 | page_field = 'page' 10 | 11 | def to_native(self, value): 12 | if not value.has_next(): 13 | return None 14 | page = value.next_page_number() 15 | request = self.context.get('request') 16 | url = request and request.get_full_path() or '' 17 | return replace_query_param(url, self.page_field, page) 18 | 19 | 20 | class PreviousPageField(serializers.Field): 21 | 22 | """Wrapper to remove absolute_uri.""" 23 | page_field = 'page' 24 | 25 | def to_native(self, value): 26 | if not value.has_previous(): 27 | return None 28 | page = value.previous_page_number() 29 | request = self.context.get('request') 30 | url = request and request.get_full_path() or '' 31 | return replace_query_param(url, self.page_field, page) 32 | 33 | 34 | class MetaSerializer(serializers.Serializer): 35 | next = NextPageField(source='*') 36 | prev = PreviousPageField(source='*') 37 | page = serializers.Field(source='number') 38 | total_count = serializers.Field(source='paginator.count') 39 | 40 | 41 | class CustomPaginationSerializer(pagination.BasePaginationSerializer): 42 | meta = MetaSerializer(source='*') # Takes the page object as the source 43 | results_field = 'objects' 44 | -------------------------------------------------------------------------------- /solitude/processor.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.conf import settings 4 | 5 | from raven.processors import Processor 6 | 7 | 8 | class JSONProcessor(Processor): 9 | 10 | """ 11 | This is a sentry wrapper to process the JSON and remove anything from it 12 | that could be considered as leaking sensitive data. Sentry has some 13 | processor for doing this, but they don't work with JSON posted in the body. 14 | """ 15 | 16 | def process(self, data, **kwargs): 17 | http = data.get('sentry.interfaces.Http', None) 18 | if not http: 19 | return data 20 | try: 21 | http['data'] = json.dumps(sanitise(json.loads(http['data']))) 22 | except (TypeError, ValueError): 23 | # At this point we've got invalid JSON so things likely went 24 | # horribly wrong. 25 | pass 26 | 27 | return data 28 | 29 | 30 | def sanitise(data, keys=None): 31 | """Sanitises keys in a dictionary.""" 32 | keys = keys or settings.SENSITIVE_DATA_KEYS 33 | 34 | def recurse(leaf): 35 | for k, v in leaf.iteritems(): 36 | if isinstance(v, dict): 37 | recurse(v) 38 | if k in keys: 39 | leaf[k] = '*' * 8 40 | 41 | try: 42 | recurse(data) 43 | except AttributeError: 44 | return data 45 | 46 | return data 47 | -------------------------------------------------------------------------------- /solitude/related_fields.py: -------------------------------------------------------------------------------- 1 | import urlparse 2 | 3 | from django.forms.fields import Field 4 | 5 | from rest_framework.relations import HyperlinkedRelatedField 6 | 7 | 8 | class RelativePathMixin(object): 9 | 10 | def get_url(self, *args, **kwargs): 11 | url = super(RelativePathMixin, self).get_url(*args, **kwargs) 12 | parsed = urlparse.urlparse(url) 13 | return parsed.path 14 | 15 | 16 | class PathRelatedField(RelativePathMixin, HyperlinkedRelatedField): 17 | pass 18 | 19 | 20 | class PathRelatedFormField(RelativePathMixin, HyperlinkedRelatedField, Field): 21 | 22 | """ 23 | A variant of the PathRelatedField that can be used in Django Forms. 24 | """ 25 | 26 | def __init__(self, *args, **kwargs): 27 | # This is in DRF 3.0, remove this in bug #416. 28 | self.allow_null = kwargs.pop('allow_null', False) 29 | 30 | queryset = kwargs.pop('queryset') 31 | super(PathRelatedFormField, self).__init__(*args, **kwargs) 32 | # __init__ sets queryset to be None, ensure we set it afterwards. 33 | self.queryset = queryset 34 | 35 | def to_python(self, value): 36 | if not value and self.allow_null: 37 | return None 38 | # Map the form method to the serializer version. 39 | return self.from_native(value) 40 | -------------------------------------------------------------------------------- /solitude/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * # noqa 2 | try: 3 | from .local import * # noqa 4 | except ImportError: 5 | print 'No local.py imported, skipping.' 6 | -------------------------------------------------------------------------------- /solitude/settings/mock-aes-sample.keys-dist: -------------------------------------------------------------------------------- 1 | this is a sample aes key file, you would never, ever use this in production, but for mock solitude on paas its just fine 2 | -------------------------------------------------------------------------------- /solitude/settings/sample.key: -------------------------------------------------------------------------------- 1 | please change this 2 | -------------------------------------------------------------------------------- /solitude/settings/sites/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/altdev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/altdev/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/altdev/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | 13 | ADMINS = () 14 | ALLOWED_HOSTS = ['payments-altdev.allizom.org', 'localhost'] 15 | 16 | DATABASES = {} 17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 20 | 21 | DEBUG = False 22 | DEBUG_PROPAGATE_EXCEPTIONS = False 23 | 24 | HMAC_KEYS = private.HMAC_KEYS 25 | 26 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 27 | 28 | LOG_LEVEL = logging.DEBUG 29 | 30 | SECRET_KEY = private.SECRET_KEY 31 | 32 | SENTRY_DSN = private.SENTRY_DSN 33 | 34 | STATSD_HOST = private.STATSD_HOST 35 | STATSD_PORT = private.STATSD_PORT 36 | STATSD_PREFIX = private.STATSD_PREFIX 37 | 38 | SYSLOG_TAG = 'http_app_payments_altdev' 39 | TEMPLATE_DEBUG = DEBUG 40 | 41 | # Solitude specific settings. 42 | AES_KEYS = private.AES_KEYS 43 | 44 | CLEANSED_SETTINGS_ACCESS = True 45 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 46 | 47 | SITE_URL = 'https://payments-altdev.allizom.org' 48 | 49 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 50 | S3_BUCKET = private.S3_BUCKET 51 | 52 | NEWRELIC_INI = None 53 | 54 | # Below is configuration of payment providers. 55 | 56 | ZIPPY_PROXY = 'https://payments-proxy-altdev.allizom.org/proxy/provider' 57 | ZIPPY_CONFIGURATION = { 58 | 'reference': { 59 | 'url': 'https://zippy-dev.allizom.org' 60 | }, 61 | } 62 | 63 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 64 | BANGO_FAKE_REFUNDS = True 65 | BANGO_PROXY = private.BANGO_PROXY 66 | BANGO_NOTIFICATION_URL = ( 67 | 'https://marketplace-altdev.allizom.org/mozpay/bango/notification') 68 | 69 | NOSE_PLUGINS = [] 70 | -------------------------------------------------------------------------------- /solitude/settings/sites/altdev/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = ['payments-proxy-altdev.allizom.org'] 13 | 14 | DATABASES = {'default': {}} 15 | 16 | DEBUG = False 17 | DEBUG_PROPAGATE_EXCEPTIONS = False 18 | 19 | HMAC_KEYS = private.HMAC_KEYS 20 | 21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 22 | 23 | LOG_LEVEL = logging.DEBUG 24 | 25 | SECRET_KEY = private.SECRET_KEY 26 | 27 | SENTRY_DSN = private.SENTRY_DSN 28 | 29 | STATSD_HOST = private.STATSD_HOST 30 | STATSD_PORT = private.STATSD_PORT 31 | STATSD_PREFIX = private.STATSD_PREFIX 32 | 33 | SYSLOG_TAG = 'http_app_payments_altdev' 34 | TEMPLATE_DEBUG = DEBUG 35 | 36 | # Solitude specific settings. 37 | AES_KEYS = {} 38 | 39 | CLEANSED_SETTINGS_ACCESS = True 40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 41 | 42 | NEWRELIC_INI = None 43 | NOSE_PLUGINS = [] 44 | 45 | REQUIRE_OAUTH = False 46 | 47 | # Below is configuration of payment providers. 48 | BANGO_ENV = 'test' 49 | BANGO_AUTH = private.BANGO_AUTH 50 | 51 | ZIPPY_CONFIGURATION = { 52 | 'reference': { 53 | 'url': 'https://zippy-dev.allizom.org', 54 | 'auth': {'key': private.ZIPPY_PAAS_KEY, 55 | 'secret': private.ZIPPY_PAAS_SECRET, 56 | 'realm': 'zippy-dev.allizom.org'} 57 | }, 58 | } 59 | -------------------------------------------------------------------------------- /solitude/settings/sites/dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/dev/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/dev/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | 13 | ADMINS = () 14 | ALLOWED_HOSTS = [ 15 | 'payments-dev.allizom.org', 16 | 'payments.dev.mozaws.net', 17 | 'localhost' 18 | ] 19 | 20 | DATABASES = {} 21 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 22 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 23 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 24 | DATABASES['default']['ATOMIC_REQUESTS'] = True 25 | 26 | DEBUG = False 27 | DEBUG_PROPAGATE_EXCEPTIONS = False 28 | 29 | HMAC_KEYS = private.HMAC_KEYS 30 | 31 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 32 | 33 | LOG_LEVEL = logging.DEBUG 34 | 35 | SECRET_KEY = private.SECRET_KEY 36 | 37 | SENTRY_DSN = private.SENTRY_DSN 38 | 39 | STATSD_HOST = private.STATSD_HOST 40 | STATSD_PORT = private.STATSD_PORT 41 | STATSD_PREFIX = private.STATSD_PREFIX 42 | 43 | SYSLOG_TAG = 'http_app_payments_dev' 44 | TEMPLATE_DEBUG = DEBUG 45 | 46 | # Solitude specific settings. 47 | AES_KEYS = private.AES_KEYS 48 | 49 | CLEANSED_SETTINGS_ACCESS = True 50 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 51 | 52 | SITE_URL = 'https://payments-dev.allizom.org' 53 | 54 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 55 | S3_BUCKET = private.S3_BUCKET 56 | 57 | NEWRELIC_INI = '/etc/newrelic.d/payments-dev.allizom.org.ini' 58 | 59 | # Below is configuration of payment providers. 60 | 61 | ZIPPY_PROXY = 'https://payments-proxy-dev.allizom.org/proxy/provider' 62 | ZIPPY_CONFIGURATION = { 63 | 'reference': { 64 | 'url': 'https://zippy-dev.allizom.org' 65 | }, 66 | } 67 | 68 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 69 | BANGO_FAKE_REFUNDS = True 70 | BANGO_PROXY = private.BANGO_PROXY 71 | BANGO_NOTIFICATION_URL = ( 72 | 'https://marketplace-dev.allizom.org/mozpay/bango/notification') 73 | 74 | NOSE_PLUGINS = [] 75 | -------------------------------------------------------------------------------- /solitude/settings/sites/dev/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = [ 13 | 'payments-proxy-dev.allizom.org', 14 | 'payments-proxy.dev.mozaws.net' 15 | ] 16 | 17 | DATABASES = {'default': {}} 18 | 19 | DEBUG = False 20 | DEBUG_PROPAGATE_EXCEPTIONS = False 21 | 22 | HMAC_KEYS = private.HMAC_KEYS 23 | 24 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 25 | 26 | LOG_LEVEL = logging.DEBUG 27 | 28 | SECRET_KEY = private.SECRET_KEY 29 | 30 | SENTRY_DSN = private.SENTRY_DSN 31 | 32 | STATSD_HOST = private.STATSD_HOST 33 | STATSD_PORT = private.STATSD_PORT 34 | STATSD_PREFIX = private.STATSD_PREFIX 35 | 36 | SYSLOG_TAG = 'http_app_payments_dev' 37 | TEMPLATE_DEBUG = DEBUG 38 | 39 | # Solitude specific settings. 40 | AES_KEYS = {} 41 | 42 | CLEANSED_SETTINGS_ACCESS = True 43 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 44 | 45 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy-dev.allizom.org.ini' 46 | NOSE_PLUGINS = [] 47 | 48 | REQUIRE_OAUTH = False 49 | 50 | # Below is configuration of payment providers. 51 | BANGO_ENV = 'test' 52 | BANGO_AUTH = private.BANGO_AUTH 53 | 54 | ZIPPY_CONFIGURATION = { 55 | 'reference': { 56 | 'url': 'https://zippy-dev.allizom.org', 57 | 'auth': {'key': private.ZIPPY_PAAS_KEY, 58 | 'secret': private.ZIPPY_PAAS_SECRET, 59 | 'realm': 'zippy-dev.allizom.org'} 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /solitude/settings/sites/paymentsalt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/paymentsalt/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/paymentsalt/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | 13 | ADMINS = () 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | DATABASES = {} 17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 20 | DATABASES['default']['ATOMIC_REQUESTS'] = True 21 | 22 | DEBUG = False 23 | DEBUG_PROPAGATE_EXCEPTIONS = False 24 | 25 | HMAC_KEYS = private.HMAC_KEYS 26 | 27 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 28 | 29 | LOG_LEVEL = logging.DEBUG 30 | 31 | SECRET_KEY = private.SECRET_KEY 32 | 33 | SENTRY_DSN = private.SENTRY_DSN 34 | 35 | STATSD_HOST = private.STATSD_HOST 36 | STATSD_PORT = private.STATSD_PORT 37 | STATSD_PREFIX = private.STATSD_PREFIX 38 | 39 | SYSLOG_TAG = 'http_app_payments_paymentsalt' 40 | TEMPLATE_DEBUG = DEBUG 41 | 42 | # Solitude specific settings. 43 | AES_KEYS = private.AES_KEYS 44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 45 | 46 | SITE_URL = 'https://payments-alt-solitude.allizom.org' 47 | 48 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 49 | S3_BUCKET = private.S3_BUCKET 50 | 51 | NEWRELIC_INI = '/etc/newrelic.d/payments-alt.allizom.org.ini' 52 | 53 | REQUIRE_OAUTH = True 54 | 55 | # Below is configuration of payment providers. 56 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 57 | BANGO_ENV = 'prod' 58 | BANGO_INSERT_STAGE = 'FROM STAGE ' 59 | BANGO_PROXY = private.BANGO_PROXY 60 | BANGO_NOTIFICATION_URL = ( 61 | 'https://payments-alt.allizom.org/mozpay/bango/notification') 62 | 63 | ZIPPY_PROXY = 'https://payments-alt-solitude-proxy.allizom.org/proxy/provider' 64 | ZIPPY_CONFIGURATION = { 65 | 'reference': { 66 | 'url': 'https://zippy-dev.allizom.org' 67 | }, 68 | } 69 | 70 | NOSE_PLUGINS = [] 71 | -------------------------------------------------------------------------------- /solitude/settings/sites/paymentsalt/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = ['*'] 13 | 14 | DATABASES = {'default': {}} 15 | 16 | DEBUG = False 17 | DEBUG_PROPAGATE_EXCEPTIONS = False 18 | 19 | HMAC_KEYS = private.HMAC_KEYS 20 | 21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 22 | 23 | LOG_LEVEL = logging.DEBUG 24 | 25 | SECRET_KEY = private.SECRET_KEY 26 | 27 | SENTRY_DSN = private.SENTRY_DSN 28 | 29 | STATSD_HOST = private.STATSD_HOST 30 | STATSD_PORT = private.STATSD_PORT 31 | STATSD_PREFIX = private.STATSD_PREFIX 32 | 33 | SYSLOG_TAG = 'http_app_payments_paymentsalt' 34 | TEMPLATE_DEBUG = DEBUG 35 | 36 | # Solitude specific settings. 37 | AES_KEYS = {} 38 | 39 | CLEANSED_SETTINGS_ACCESS = True 40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 41 | 42 | NEWRELIC_INI = '/etc/newrelic.d/payments-alt-proxy.allizom.org.ini' 43 | NOSE_PLUGINS = [] 44 | 45 | REQUIRE_OAUTH = False 46 | SITE_URL = 'https://payments-alt-solitude-proxy.allizom.org' 47 | 48 | # Below is configuration of payment providers. 49 | BANGO_AUTH = private.BANGO_AUTH 50 | BANGO_ENV = 'prod' 51 | 52 | ZIPPY_CONFIGURATION = { 53 | 'reference': { 54 | 'url': 'https://zippy-dev.allizom.org', 55 | 'auth': {'key': private.ZIPPY_PAAS_KEY, 56 | 'secret': private.ZIPPY_PAAS_SECRET, 57 | 'realm': 'zippy-dev.allizom.org'} 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /solitude/settings/sites/prod/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/prod/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/prod/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | ADMINS = () 13 | ALLOWED_HOSTS = ('*',) 14 | 15 | DATABASES = {} 16 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 17 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 18 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 19 | DATABASES['default']['ATOMIC_REQUESTS'] = True 20 | 21 | DEBUG = False 22 | DEBUG_PROPAGATE_EXCEPTIONS = False 23 | 24 | HMAC_KEYS = private.HMAC_KEYS 25 | 26 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 27 | 28 | LOG_LEVEL = logging.DEBUG 29 | 30 | SECRET_KEY = private.SECRET_KEY 31 | 32 | SENTRY_DSN = private.SENTRY_DSN 33 | 34 | STATSD_HOST = private.STATSD_HOST 35 | STATSD_PORT = private.STATSD_PORT 36 | STATSD_PREFIX = private.STATSD_PREFIX 37 | 38 | SYSLOG_TAG = 'http_app_payments' 39 | TEMPLATE_DEBUG = DEBUG 40 | 41 | # Solitude specific settings. 42 | AES_KEYS = private.AES_KEYS 43 | 44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 45 | 46 | REQUIRE_OAUTH = True 47 | 48 | SITE_URL = 'https://payments.firefox.com' 49 | 50 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 51 | S3_BUCKET = private.S3_BUCKET 52 | 53 | # Below is configuration of payment providers. 54 | 55 | ZIPPY_CONFIGURATION = {} 56 | 57 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 58 | BANGO_ENV = 'prod' 59 | BANGO_FAKE_REFUNDS = False 60 | BANGO_PROXY = private.BANGO_PROXY 61 | BANGO_NOTIFICATION_URL = ( 62 | 'https://marketplace.firefox.com/mozpay/bango/notification') 63 | 64 | NOSE_PLUGINS = [] 65 | -------------------------------------------------------------------------------- /solitude/settings/sites/prod/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = ('*',) 13 | 14 | DATABASES = {'default': {}} 15 | 16 | DEBUG = False 17 | DEBUG_PROPAGATE_EXCEPTIONS = False 18 | 19 | HMAC_KEYS = private.HMAC_KEYS 20 | 21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 22 | 23 | LOG_LEVEL = logging.DEBUG 24 | 25 | SECRET_KEY = private.SECRET_KEY 26 | 27 | SENTRY_DSN = private.SENTRY_DSN 28 | 29 | STATSD_HOST = private.STATSD_HOST 30 | STATSD_PORT = private.STATSD_PORT 31 | STATSD_PREFIX = private.STATSD_PREFIX 32 | 33 | SYSLOG_TAG = 'http_app_payments' 34 | TEMPLATE_DEBUG = DEBUG 35 | 36 | 37 | # Solitude specific settings. 38 | AES_KEYS = {} 39 | 40 | CLEANSED_SETTINGS_ACCESS = True 41 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 42 | NOSE_PLUGINS = [] 43 | 44 | REQUIRE_OAUTH = False 45 | SITE_URL = private.SITE_URL 46 | 47 | # Below is configuration of payment providers. 48 | ZIPPY_CONFIGURATION = {} 49 | 50 | BANGO_AUTH = private.BANGO_AUTH 51 | BANGO_ENV = 'prod' 52 | -------------------------------------------------------------------------------- /solitude/settings/sites/s3dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/s3dev/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/s3dev/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | 13 | ADMINS = () 14 | ALLOWED_HOSTS = [ 15 | 'payments-s3dev.allizom.org' 16 | ] 17 | 18 | DATABASES = {} 19 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 20 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 21 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 22 | DATABASES['default']['ATOMIC_REQUESTS'] = True 23 | 24 | DEBUG = False 25 | DEBUG_PROPAGATE_EXCEPTIONS = False 26 | 27 | HMAC_KEYS = private.HMAC_KEYS 28 | 29 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 30 | 31 | LOG_LEVEL = logging.DEBUG 32 | 33 | SECRET_KEY = private.SECRET_KEY 34 | 35 | SENTRY_DSN = private.SENTRY_DSN 36 | 37 | STATSD_HOST = private.STATSD_HOST 38 | STATSD_PORT = private.STATSD_PORT 39 | STATSD_PREFIX = private.STATSD_PREFIX 40 | 41 | SYSLOG_TAG = 'http_app_payments_s3dev' 42 | TEMPLATE_DEBUG = DEBUG 43 | 44 | # Solitude specific settings. 45 | AES_KEYS = private.AES_KEYS 46 | 47 | CLEANSED_SETTINGS_ACCESS = True 48 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 49 | 50 | SITE_URL = 'https://payments-s3dev.allizom.org' 51 | 52 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 53 | S3_BUCKET = private.S3_BUCKET 54 | 55 | NEWRELIC_INI = '/etc/newrelic.d/payments-s3dev.allizom.org.ini' 56 | 57 | # Below is configuration of payment providers. 58 | 59 | ZIPPY_PROXY = 'https://payments-proxy-s3dev.allizom.org/proxy/provider' 60 | ZIPPY_CONFIGURATION = { 61 | 'reference': { 62 | 'url': 'https://zippy-dev.allizom.org' 63 | }, 64 | } 65 | 66 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 67 | BANGO_FAKE_REFUNDS = True 68 | BANGO_PROXY = private.BANGO_PROXY 69 | BANGO_NOTIFICATION_URL = ( 70 | 'https://marketplace-s3dev.allizom.org/mozpay/bango/notification') 71 | 72 | NOSE_PLUGINS = [] 73 | -------------------------------------------------------------------------------- /solitude/settings/sites/s3dev/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = [ 13 | 'payments-proxy-s3dev.allizom.org' 14 | ] 15 | 16 | DATABASES = {'default': {}} 17 | 18 | DEBUG = False 19 | DEBUG_PROPAGATE_EXCEPTIONS = False 20 | 21 | HMAC_KEYS = private.HMAC_KEYS 22 | 23 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 24 | 25 | LOG_LEVEL = logging.DEBUG 26 | 27 | SECRET_KEY = private.SECRET_KEY 28 | 29 | SENTRY_DSN = private.SENTRY_DSN 30 | 31 | STATSD_HOST = private.STATSD_HOST 32 | STATSD_PORT = private.STATSD_PORT 33 | STATSD_PREFIX = private.STATSD_PREFIX 34 | 35 | SYSLOG_TAG = 'http_app_payments_s3dev' 36 | TEMPLATE_DEBUG = DEBUG 37 | 38 | # Solitude specific settings. 39 | AES_KEYS = {} 40 | 41 | CLEANSED_SETTINGS_ACCESS = True 42 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 43 | 44 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy-s3dev.allizom.org.ini' 45 | NOSE_PLUGINS = [] 46 | 47 | REQUIRE_OAUTH = False 48 | 49 | # Below is configuration of payment providers. 50 | BANGO_ENV = 'test' 51 | BANGO_AUTH = private.BANGO_AUTH 52 | 53 | ZIPPY_CONFIGURATION = { 54 | 'reference': { 55 | 'url': 'https://zippy-dev.allizom.org', 56 | 'auth': {'key': private.ZIPPY_PAAS_KEY, 57 | 'secret': private.ZIPPY_PAAS_SECRET, 58 | 'realm': 'zippy-dev.allizom.org'} 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /solitude/settings/sites/stage/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/settings/sites/stage/__init__.py -------------------------------------------------------------------------------- /solitude/settings/sites/stage/db.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import dj_database_url 6 | 7 | import private_base as private 8 | 9 | from solitude.settings import base 10 | from django_sha2 import get_password_hashers 11 | 12 | 13 | ADMINS = () 14 | ALLOWED_HOSTS = ['*'] 15 | 16 | DATABASES = {} 17 | DATABASES['default'] = dj_database_url.parse(private.DATABASES_DEFAULT_URL) 18 | DATABASES['default']['ENGINE'] = 'django.db.backends.mysql' 19 | DATABASES['default']['OPTIONS'] = {'init_command': 'SET storage_engine=InnoDB'} 20 | DATABASES['default']['ATOMIC_REQUESTS'] = True 21 | 22 | DEBUG = False 23 | DEBUG_PROPAGATE_EXCEPTIONS = False 24 | 25 | HMAC_KEYS = private.HMAC_KEYS 26 | 27 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 28 | 29 | LOG_LEVEL = logging.DEBUG 30 | 31 | SECRET_KEY = private.SECRET_KEY 32 | 33 | SENTRY_DSN = private.SENTRY_DSN 34 | 35 | STATSD_HOST = private.STATSD_HOST 36 | STATSD_PORT = private.STATSD_PORT 37 | STATSD_PREFIX = private.STATSD_PREFIX 38 | 39 | SYSLOG_TAG = 'http_app_payments_stage' 40 | TEMPLATE_DEBUG = DEBUG 41 | 42 | # Solitude specific settings. 43 | AES_KEYS = private.AES_KEYS 44 | CLIENT_OAUTH_KEYS = private.CLIENT_OAUTH_KEYS 45 | 46 | SITE_URL = 'https://payments.allizom.org' 47 | 48 | S3_AUTH = {'key': private.S3_AUTH_KEY, 'secret': private.S3_AUTH_SECRET} 49 | S3_BUCKET = private.S3_BUCKET 50 | 51 | REQUIRE_OAUTH = True 52 | 53 | NEWRELIC_INI = '/etc/newrelic.d/payments.allizom.org.ini' 54 | 55 | ZIPPY_CONFIGURATION = {} 56 | 57 | # Below is configuration of payment providers. 58 | BANGO_BASIC_AUTH = private.BANGO_BASIC_AUTH 59 | BANGO_ENV = 'prod' 60 | BANGO_FAKE_REFUNDS = False 61 | BANGO_INSERT_STAGE = 'FROM STAGE ' 62 | BANGO_PROXY = private.BANGO_PROXY 63 | BANGO_NOTIFICATION_URL = ( 64 | 'https://marketplace.allizom.org/mozpay/bango/notification') 65 | 66 | NOSE_PLUGINS = [] 67 | -------------------------------------------------------------------------------- /solitude/settings/sites/stage/proxy.py: -------------------------------------------------------------------------------- 1 | """private_base will be populated from puppet and placed in this directory""" 2 | 3 | import logging 4 | 5 | import private_base as private 6 | 7 | from solitude.settings import base 8 | from django_sha2 import get_password_hashers 9 | 10 | 11 | ADMINS = () 12 | ALLOWED_HOSTS = ['*'] 13 | 14 | DATABASES = {'default': {}} 15 | 16 | DEBUG = False 17 | DEBUG_PROPAGATE_EXCEPTIONS = False 18 | 19 | HMAC_KEYS = private.HMAC_KEYS 20 | 21 | PASSWORD_HASHERS = get_password_hashers(base.BASE_PASSWORD_HASHERS, HMAC_KEYS) 22 | 23 | LOG_LEVEL = logging.DEBUG 24 | 25 | SECRET_KEY = private.SECRET_KEY 26 | 27 | SENTRY_DSN = private.SENTRY_DSN 28 | 29 | STATSD_HOST = private.STATSD_HOST 30 | STATSD_PORT = private.STATSD_PORT 31 | STATSD_PREFIX = private.STATSD_PREFIX 32 | 33 | SYSLOG_TAG = 'http_app_payments_stage' 34 | TEMPLATE_DEBUG = DEBUG 35 | 36 | # Solitude specific settings. 37 | AES_KEYS = {} 38 | 39 | CLEANSED_SETTINGS_ACCESS = True 40 | CLIENT_JWT_KEYS = private.CLIENT_JWT_KEYS 41 | 42 | NEWRELIC_INI = '/etc/newrelic.d/payments-proxy.allizom.org.ini' 43 | NOSE_PLUGINS = [] 44 | 45 | REQUIRE_OAUTH = False 46 | 47 | # Below is configuration of payment providers. 48 | BANGO_ENV = 'prod' 49 | BANGO_AUTH = private.BANGO_AUTH 50 | 51 | ZIPPY_CONFIGURATION = {} 52 | -------------------------------------------------------------------------------- /solitude/settings/test.py: -------------------------------------------------------------------------------- 1 | # test_utils picks this file up for testing. 2 | import atexit 3 | import os 4 | import shutil 5 | from tempfile import gettempdir 6 | 7 | from solitude.settings.base import * # noqa 8 | 9 | filename = os.path.join(os.path.dirname(__file__), 'sample.key') 10 | 11 | AES_KEYS = { 12 | 'buyeremail:key': filename, 13 | 'sellerproduct:secret': filename, 14 | 'bango:signature': filename, 15 | } 16 | 17 | SOLITUDE_PROXY = False 18 | 19 | if os.environ.get('SOLITUDE_PROXY', 'disabled') == 'enabled': 20 | raise ValueError('You have the environment variable SOLITUDE_PROXY set to ' 21 | '"enabled", this breaks the tests, aborting.') 22 | 23 | PAYPAL_PROXY = False 24 | PAYPAL_URLS_ALLOWED = ('https://marketplace-dev.allizom.org',) 25 | 26 | DEBUG = True 27 | DEBUG_PROPAGATE_EXCEPTIONS = False 28 | 29 | DUMP_REQUESTS = False 30 | 31 | HMAC_KEYS = {'2011-01-01': 'cheesecake'} 32 | from django_sha2 import get_password_hashers 33 | PASSWORD_HASHERS = get_password_hashers(BASE_PASSWORD_HASHERS, HMAC_KEYS) 34 | 35 | # Celery. 36 | CELERY_ALWAYS_EAGER = True 37 | 38 | # Send all statsd to nose. 39 | STATSD_CLIENT = 'django_statsd.clients.nose' 40 | 41 | # No need for paranoia in tests. 42 | from django_paranoia.signals import process 43 | process.disconnect(dispatch_uid='paranoia.reporter.django_paranoia' 44 | '.reporters.cef_') 45 | process.disconnect(dispatch_uid='paranoia.reporter.django_paranoia' 46 | '.reporters.log') 47 | DJANGO_PARANOIA_REPORTERS = [] 48 | 49 | # We don't want to hit the live servers in tests. 50 | BANGO_MOCK = True 51 | ZIPPY_MOCK = True 52 | 53 | BRAINTREE_MERCHANT_ID = 'test' 54 | BRAINTREE_PROXY = 'http://m.o' 55 | 56 | SITE_URL = 'http://localhost/' 57 | 58 | SEND_USER_ID_TO_BANGO = True 59 | CHECK_BANGO_TOKEN = True 60 | 61 | REQUIRE_OAUTH = False 62 | 63 | # Live server tests require this, otherwise they will fail. There is no static 64 | # content on our site, so meh. 65 | STATIC_URL = '/' 66 | STATIC_ROOT = '/' 67 | 68 | # Not using bcrypt for hashing is a major test speed up. 69 | PASSWORD_HASHERS = ( 70 | 'django.contrib.auth.hashers.MD5PasswordHasher', 71 | ) 72 | 73 | 74 | # Suds keeps a cache of the WSDL around, so after completing the test run, 75 | # lets remove that so it doesn't affect the next test run. 76 | def _cleanup(): 77 | target = os.path.join(gettempdir(), 'suds') 78 | if os.path.exists(target): 79 | shutil.rmtree(target) 80 | 81 | atexit.register(_cleanup) 82 | -------------------------------------------------------------------------------- /solitude/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/solitude/tests/__init__.py -------------------------------------------------------------------------------- /solitude/tests/live.py: -------------------------------------------------------------------------------- 1 | 2 | from django.test import LiveServerTestCase 3 | from django.test.utils import override_settings 4 | 5 | from nose.plugins.attrib import attr 6 | 7 | from curling import lib 8 | from solitude.base import getLogger 9 | 10 | log = getLogger('s.tests') 11 | 12 | configs = { 13 | 'REQUIRE_OAUTH': True, 14 | 'SITE_URL': 'http://localhost:8081', 15 | 'CLIENT_OAUTH_KEYS': {'foo': 'bar'}, 16 | 'DEBUG': False, 17 | 'DEBUG_PROPAGATE_EXCEPTIONS': False, 18 | } 19 | 20 | 21 | @attr('live') 22 | @override_settings(**configs) 23 | class LiveTestCase(LiveServerTestCase): 24 | 25 | @property 26 | def request(self): 27 | api = lib.API(self.live_server_url) 28 | api.activate_oauth(*configs['CLIENT_OAUTH_KEYS'].items()[0]) 29 | return api 30 | -------------------------------------------------------------------------------- /solitude/tests/resources.py: -------------------------------------------------------------------------------- 1 | from tastypie import fields 2 | 3 | from solitude.base import Resource 4 | 5 | 6 | class Fake(object): 7 | name = '' 8 | pk = 0 9 | 10 | 11 | class FakeResource(Resource): 12 | name = fields.CharField(attribute='name') 13 | 14 | class Meta(Resource.Meta): 15 | resource_name = 'fake' 16 | list_allowed_methods = ['post'] 17 | object_class = Fake 18 | 19 | def obj_create(self, bundle, request, **kwargs): 20 | bundle.obj = Fake() 21 | return bundle 22 | 23 | def get_resource_uri(self, bundle): 24 | return '/' 25 | -------------------------------------------------------------------------------- /solitude/tests/test_authentication.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.conf import settings 3 | from django.test import RequestFactory 4 | 5 | from mock import patch 6 | from nose.tools import eq_, ok_, raises 7 | from rest_framework.exceptions import AuthenticationFailed 8 | from slumber.exceptions import HttpClientError 9 | 10 | from solitude.authentication import Consumer, RestOAuthAuthentication 11 | from solitude.tests.live import LiveTestCase 12 | 13 | keys = {'foo': 'bar'} 14 | keys_dict = {'key': 'foo', 'secret': 'bar'} 15 | 16 | 17 | @patch.object(settings, 'CLIENT_OAUTH_KEYS', keys) 18 | class TestDRFAuthentication(test.TestCase): 19 | 20 | def setUp(self): 21 | self.authentication = RestOAuthAuthentication() 22 | self.factory = RequestFactory() 23 | self.consumer = Consumer(*keys.items()[0]) 24 | 25 | def test_skip(self): 26 | req = self.factory.get('/skip-oauth/') 27 | with self.settings(REQUIRE_OAUTH=True, SKIP_OAUTH=['/skip-oauth/']): 28 | ok_(self.authentication.authenticate(req)) 29 | 30 | @raises(AuthenticationFailed) 31 | def test_not_skip(self): 32 | req = self.factory.get('/require-oauth/') 33 | with self.settings(REQUIRE_OAUTH=True, SKIP_OAUTH=['/skip-oauth/']): 34 | eq_(self.authentication.authenticate(req)) 35 | 36 | 37 | class TestAuthentication(LiveTestCase): 38 | 39 | def test_valid_auth(self): 40 | # If there was an error with auth, an assertion would be raised. 41 | self.request.by_url('/generic/transaction/').get() 42 | 43 | def test_invalid_auth(self): 44 | with self.settings(CLIENT_OAUTH_KEYS={'f': 'b'}): 45 | with self.assertRaises(HttpClientError) as err: 46 | self.request.by_url('/generic/transaction/').get() 47 | eq_(err.exception.response.status_code, 403) 48 | -------------------------------------------------------------------------------- /solitude/tests/test_error.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django import test 3 | from django.db import transaction 4 | 5 | from nose.tools import eq_, ok_ 6 | from slumber.exceptions import HttpClientError 7 | 8 | from curling import lib 9 | from solitude.errors import FormError, MozillaFormatter 10 | from solitude.exceptions import custom_exception_handler 11 | from solitude.tests.live import LiveTestCase 12 | 13 | 14 | class TestErrors(LiveTestCase): 15 | 16 | def test_not_found(self): 17 | with self.assertRaises(HttpClientError) as error: 18 | self.request.by_url('/this/does-not-exist/').get() 19 | eq_(error.exception.response.json, {}) 20 | 21 | def test_403_html(self): 22 | with self.assertRaises(HttpClientError) as error: 23 | lib.API(self.live_server_url).by_url('/this/does-not-exist/').get() 24 | eq_(error.exception.response.json, {}) 25 | 26 | def test_drf_403_html(self): 27 | with self.assertRaises(HttpClientError) as error: 28 | lib.API(self.live_server_url).by_url('/generic/transaction/').get() 29 | eq_(error.exception.response.json, 30 | {'detail': 'Incorrect authentication credentials.'}) 31 | 32 | 33 | class TestRollback(test.TestCase): 34 | 35 | def test_did_rollback(self): 36 | custom_exception_handler(Exception()) 37 | ok_(transaction.get_connection().get_rollback()) 38 | 39 | 40 | class DummyForm(forms.Form): 41 | name = forms.CharField() 42 | 43 | def clean_name(self): 44 | raise forms.ValidationError([ 45 | forms.ValidationError('First error', 'first'), 46 | forms.ValidationError('Second error', 'second') 47 | ]) 48 | 49 | def clean(self): 50 | raise forms.ValidationError('Non field error', code='non-field') 51 | 52 | 53 | class TestFormatting(test.TestCase): 54 | 55 | def setUp(self): 56 | self.moz = MozillaFormatter 57 | self.form = DummyForm({'name': 'bob'}) 58 | ok_(not self.form.is_valid()) 59 | 60 | def test_form_error(self): 61 | eq_(self.moz(FormError(self.form.errors)).format(), 62 | {'mozilla': 63 | {'name': [ 64 | {'message': u'First error', 'code': 'first'}, 65 | {'message': u'Second error', 'code': 'second'} 66 | ], '__all__': [ 67 | {'message': u'Non field error', 'code': 'non-field'} 68 | ]} 69 | }) 70 | -------------------------------------------------------------------------------- /solitude/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from django.test import RequestFactory, TestCase 2 | 3 | from nose.tools import eq_ 4 | 5 | from solitude.logger import get_oauth_key, get_transaction_id 6 | from solitude.middleware import LoggerMiddleware 7 | 8 | 9 | class TestMiddleware(TestCase): 10 | 11 | def test_nothing(self): 12 | req = RequestFactory().get('/') 13 | LoggerMiddleware().process_request(req) 14 | eq_(get_oauth_key(), '') 15 | eq_(get_transaction_id(), '-') 16 | 17 | def test_something(self): 18 | req = RequestFactory().get('/', HTTP_TRANSACTION_ID='foo') 19 | req.OAUTH_KEY = 'bar' 20 | LoggerMiddleware().process_request(req) 21 | eq_(get_oauth_key(), 'bar') 22 | eq_(get_transaction_id(), 'foo') 23 | -------------------------------------------------------------------------------- /solitude/tests/test_processor.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | 3 | from solitude.processor import JSONProcessor, sanitise 4 | 5 | 6 | sanitise_dicts = [ 7 | [None, None], 8 | [{'foo': 'bar'}, {'foo': 'bar'}], 9 | [{'foo': {'pin': 'bar'}}, {'foo': {'pin': '*' * 8}}] 10 | ] 11 | 12 | process_data = [ 13 | [{'sentry.interfaces.Http': {'data': '{"pin": "1234"}'}}, 14 | {'sentry.interfaces.Http': {'data': '{"pin": "********"}'}}], 15 | # All the following remain unchanged. 16 | [{'sentry.interfaces.Http': {}}, 17 | {'sentry.interfaces.Http': {}}], 18 | [{'sentry.interfaces.Http': 'None'}, 19 | {'sentry.interfaces.Http': 'None'}], 20 | [{'sentry.interfaces.Http': {'data': 'blargh!'}}, 21 | {'sentry.interfaces.Http': {'data': 'blargh!'}}], 22 | [{}, {}], 23 | ] 24 | 25 | 26 | def test_processor(): 27 | for value, expected in process_data: 28 | eq_(JSONProcessor('').process(value), expected) 29 | 30 | 31 | def test_sanitise(): 32 | for value, expected in sanitise_dicts: 33 | eq_(sanitise(value, ['pin']), expected) 34 | -------------------------------------------------------------------------------- /solitude/urls.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf.urls import include, patterns, url 4 | 5 | 6 | services_patterns = patterns( 7 | 'lib.services.resources', 8 | url(r'^settings/$', 'settings_list', name='services.settings'), 9 | url(r'^settings/(?P[^/<>]+)/$', 'settings_view', 10 | name='services.setting'), 11 | url(r'^error/', 'error', name='services.error'), 12 | url(r'^logs/', 'logs', name='services.log'), 13 | url(r'^status/', 'status', name='services.status'), 14 | url(r'^request/', 'request_resource', name='services.request'), 15 | url(r'^failures/transactions/', 'transactions_failures', 16 | name='services.failures.transactions'), 17 | url(r'^failures/statuses/', 'statuses_failures', 18 | name='services.failures.statuses'), 19 | ) 20 | 21 | generic_urls = patterns( 22 | '', 23 | url('', include('lib.buyers.urls')), 24 | url('', include('lib.sellers.urls')), 25 | url('', include('lib.transactions.urls')), 26 | ) 27 | 28 | urls = [ 29 | url(r'^$', 'solitude.views.home', name='home'), 30 | url(r'^generic/', include(generic_urls, namespace='generic')), 31 | url(r'^proxy/', include('lib.proxy.urls')), 32 | url(r'^bango/', include('lib.bango.urls', namespace='bango')), 33 | url(r'^braintree/', include('lib.brains.urls', namespace='braintree')), 34 | url(r'^provider/', include('lib.provider.urls')), 35 | url(r'^services/', include(services_patterns)) 36 | ] 37 | 38 | if os.getenv('IS_DOCKER'): 39 | urls.append(url(r'^solitude/services/', include(services_patterns))) 40 | 41 | urlpatterns = patterns('', *urls) 42 | 43 | handler500 = 'solitude.views.error_500' 44 | handler404 = 'solitude.views.error_404' 45 | handler403 = 'solitude.views.error_403' 46 | -------------------------------------------------------------------------------- /solitude/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | 4 | 5 | # Note deliberately ignoring o, O, 0, i, l and 1 as too similar. 6 | choices = 'abcdefghjklmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' 7 | 8 | 9 | def shorter(integer): 10 | """ 11 | Convert an integer into something shorter. 12 | """ 13 | output = [] 14 | while integer > 0: 15 | remainder = integer % len(choices) 16 | output.append(choices[remainder]) 17 | integer /= len(choices) 18 | return ''.join(reversed(output)) 19 | 20 | 21 | def validate_settings(): 22 | """ 23 | Validate that if not in DEBUG mode, key settings have been changed. 24 | """ 25 | if settings.DEBUG: 26 | return 27 | 28 | # Things that values must not be. 29 | for key, value in [ 30 | ('SECRET_KEY', 'please change this'), 31 | ('HMAC_KEYS', {'2011-01-01': 'please change me'})]: 32 | if getattr(settings, key) == value: 33 | raise ImproperlyConfigured('{0} must be changed from default' 34 | .format(key)) 35 | 36 | for key, value in settings.AES_KEYS.items(): 37 | if value == 'solitude/settings/sample.key': 38 | raise ImproperlyConfigured('AES_KEY {0} must be changed from ' 39 | 'default'.format(key)) 40 | 41 | if settings.REQUIRE_OAUTH: 42 | for key, value in settings.CLIENT_OAUTH_KEYS.items(): 43 | if value == 'please change this': 44 | raise ImproperlyConfigured('CLIENT_OAUTH_KEYS {0} must be ' 45 | 'changed from'.format(key)) 46 | -------------------------------------------------------------------------------- /solitude/views.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from django.http import JsonResponse 4 | 5 | from solitude.base import log_cef 6 | 7 | 8 | def home(request): 9 | return response(request, 10 | data={'message': 'A home page in solitude.'}, 11 | status=200) 12 | 13 | 14 | def response(request, status=500, data=None): 15 | # If returning JSON, then we can't send back an empty body, unless we 16 | # return a 204 - the client should be able to parse it. 17 | # 18 | # This assumes the client can receive JSON. We could send a HTTP 406 19 | # to anyone not accepting JSON, but that seems unusually cruel punishment 20 | # and will mask the real error. 21 | message = data if data else {} 22 | return JsonResponse(message, status=status) 23 | 24 | 25 | def error_500(request): 26 | exception = sys.exc_info()[1] 27 | log_cef(str(exception), request, severity=3) 28 | return response(request, status=500, 29 | data={'error': exception.__class__.__name__}) 30 | 31 | 32 | def error_403(request): 33 | return response(request, status=403) 34 | 35 | 36 | def error_404(request): 37 | return response(request, status=404) 38 | -------------------------------------------------------------------------------- /wsgi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/solitude/ed7e0b93245ba51cab391b224c29465088e72e03/wsgi/__init__.py -------------------------------------------------------------------------------- /wsgi/playdoh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import site 3 | 4 | os.environ.setdefault('CELERY_LOADER', 'django') 5 | # NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override 6 | # the default value in manage.py 7 | 8 | # Add the app dir to the python path so we can import manage. 9 | wsgidir = os.path.dirname(__file__) 10 | site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../'))) 11 | 12 | # manage adds /apps, /lib, and /vendor to the Python path. 13 | import manage # noqa 14 | 15 | from django.core.wsgi import get_wsgi_application 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /wsgi/proxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | import site 3 | 4 | os.environ.setdefault('SOLITUDE_PROXY', 'enabled') 5 | os.environ.setdefault('CELERY_LOADER', 'django') 6 | # NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override 7 | # the default value in manage.py 8 | 9 | # Add the app dir to the python path so we can import manage. 10 | wsgidir = os.path.dirname(__file__) 11 | site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../'))) 12 | 13 | # manage adds /apps, /lib, and /vendor to the Python path. 14 | import manage # noqa 15 | 16 | from django.core.wsgi import get_wsgi_application 17 | application = get_wsgi_application() 18 | --------------------------------------------------------------------------------