From 72bc2d34a4a9294435c00679b941050cf696b4b3 Mon Sep 17 00:00:00 2001
From: alice {}
", j)
-
-
-class PaymentAdmin(admin.ModelAdmin):
- model = Payment
- list_display = ('user', 'backend', 'status', 'amount', 'paid_amount', 'created')
- list_filter = ('backend_id', 'status')
-
- fieldsets = (
- (None, {
- 'fields': ('backend', 'user_link', 'subscription_link', 'time', 'status',
- 'status_message'),
- }),
- (_("Payment Data"), {
- 'fields': ('amount_fmt', 'paid_amount_fmt',
- 'backend_extid_link', 'backend_data_fmt'),
- }),
- )
-
- readonly_fields = ('backend', 'user_link', 'time', 'status', 'status_message',
- 'amount_fmt', 'paid_amount_fmt', 'subscription_link',
- 'backend_extid_link', 'backend_data_fmt')
- search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data')
-
- def backend(self, object):
- try:
- return object.backend.backend_verbose_name
- except KeyError:
- return "#" + object.backend_id
-
- def backend_data_fmt(self, object):
- return json_format(object.backend_data)
-
- def backend_extid_link(self, object):
- try:
- ext_url = object.backend.get_ext_url(object)
- return link(object.backend_extid, ext_url)
- except KeyError:
- return "#" + object.backend_id
-
- def amount_fmt(self, object):
- return '%.2f %s' % (object.amount / 100, object.currency_name)
- amount_fmt.short_description = _("Amount")
-
- def paid_amount_fmt(self, object):
- return '%.2f %s' % (object.paid_amount / 100, object.currency_name)
- paid_amount_fmt.short_description = _("Paid amount")
-
- def user_link(self, object):
- change_url = resolve_url('admin:auth_user_change', object.user.id)
- return link(object.user.username, change_url)
- user_link.short_description = 'User'
-
- def subscription_link(self, object):
- change_url = resolve_url('admin:payments_subscription_change',
- object.subscription.id)
- return link(object.subscription.id, change_url)
- subscription_link.short_description = 'Subscription'
-
-
-class SubscriptionAdmin(admin.ModelAdmin):
- model = Subscription
- list_display = ('user', 'created', 'status', 'backend', 'backend_extid')
- list_filter = ('backend_id', 'status')
- readonly_fields = ('user_link', 'backend', 'created', 'status',
- 'last_confirmed_payment', 'payments_links',
- 'backend_extid_link', 'backend_data_fmt')
- search_fields = ('user__username', 'user__email', 'backend_extid', 'backend_data')
- actions = (subscr_mark_as_cancelled,)
- fieldsets = (
- (None, {
- 'fields': ('backend', 'user_link', 'payments_links', 'status',
- 'last_confirmed_payment'),
- }),
- (_("Payment Data"), {
- 'fields': ('backend_extid_link', 'backend_data_fmt'),
- }),
- )
-
- def backend(self, object):
- return object.backend.backend_verbose_name
-
- def backend_data_fmt(self, object):
- return json_format(object.backend_data)
-
- def user_link(self, object):
- change_url = resolve_url('admin:auth_user_change', object.user.id)
- return link(object.user.id, change_url)
- user_link.short_description = 'User'
-
- def payments_links(self, object):
- count = Payment.objects.filter(subscription=object).count()
- payments_url = resolve_url('admin:payments_payment_changelist')
- url = "%s?subscription__id__exact=%s" % (payments_url, object.id)
- return link("%d payment(s)" % count, url)
- payments_links.short_description = 'Payments'
-
- def backend_extid_link(self, object):
- ext_url = object.backend.get_subscr_ext_url(object)
- return link(object.backend_extid, ext_url)
- backend_extid_link.allow_tags = True
-
-class FeedbackAdmin(admin.ModelAdmin):
- model = Feedback
- list_display = ('user', 'created', 'short_message')
- readonly_fields = ('user', 'created', 'message', 'subscription')
-
- def short_message(self, obj):
- return Truncator(obj.message).chars(80)
-
-admin.site.register(Payment, PaymentAdmin)
-admin.site.register(Subscription, SubscriptionAdmin)
-admin.site.register(Feedback, FeedbackAdmin)
-
diff --git a/payments/backends/__init__.py b/payments/backends/__init__.py
deleted file mode 100644
index b0f5ef2..0000000
--- a/payments/backends/__init__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# flake8: noqa
-
-from .base import BackendBase, ManualBackend
-from .paypal import PaypalBackend
-from .bitcoin import BitcoinBackend
-from .stripe import StripeBackend
-from .coinpayments import CoinPaymentsBackend
-
diff --git a/payments/backends/base.py b/payments/backends/base.py
deleted file mode 100644
index 82c7170..0000000
--- a/payments/backends/base.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-
-
-class BackendBase:
- backend_id = None
- backend_verbose_name = ""
- backend_display_name = ""
- backend_enabled = False
- backend_has_recurring = False
-
- def __init__(self, settings):
- pass
-
- def new_payment(self, payment):
- """ Initialize a payment and returns an URL to redirect the user.
- Can return a HTML string that will be sent back to the user in a
- default template (like a form) or a HTTP response (like a redirect).
- """
- raise NotImplementedError()
-
- def callback(self, payment, request):
- """ Handle a callback """
- raise NotImplementedError()
-
- def callback_subscr(self, payment, request):
- """ Handle a callback (recurring payments) """
- raise NotImplementedError()
-
- def cancel_subscription(self, subscr):
- """ Cancel a subscription """
- raise NotImplementedError()
-
- def get_info(self):
- """ Returns some status (key, value) list """
- return ()
-
- def get_ext_url(self, payment):
- """ Returns URL to external payment view, or None """
- return None
-
- def get_subscr_ext_url(self, subscr):
- """ Returns URL to external payment view, or None """
- return None
-
-
-class ManualBackend(BackendBase):
- """ Manual backend used to store and display informations about a
- payment processed manually.
- More a placeholder than an actual payment beckend, everything raises
- NotImplementedError().
- """
-
- backend_id = 'manual'
- backend_verbose_name = _("Manual")
-
-
diff --git a/payments/backends/bitcoin.py b/payments/backends/bitcoin.py
deleted file mode 100644
index 26a075b..0000000
--- a/payments/backends/bitcoin.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from decimal import Decimal
-
-from django.shortcuts import redirect
-from django.utils.translation import ugettext_lazy as _
-from django.urls import reverse
-from constance import config as site_config
-
-from .base import BackendBase
-
-
-class BitcoinBackend(BackendBase):
- """ Bitcoin backend.
- Connects to a bitcoind.
- """
- backend_id = 'bitcoin'
- backend_verbose_name = _("Bitcoin")
- backend_display_name = _("Bitcoin")
-
- COIN = 100000000
-
- def __init__(self, settings):
- from bitcoin import SelectParams
- from bitcoin.rpc import Proxy
-
- self.account = settings.get('account', 'ccvpn3')
-
- chain = settings.get('chain')
- if chain:
- SelectParams(chain)
-
- self.url = settings.get('url')
- if not self.url:
- return
-
- self.make_rpc = lambda: Proxy(self.url)
- self.rpc = self.make_rpc()
- self.backend_enabled = True
-
- @property
- def btc_value(self):
- return site_config.BTC_EUR_VALUE
-
- def new_payment(self, payment):
- rpc = self.make_rpc()
-
- # bitcoins amount = (amount in cents) / (cents per bitcoin)
- btc_price = round(Decimal(payment.amount) / self.btc_value, 5)
-
- address = str(rpc.getnewaddress(self.account))
-
- msg = _("Please send %(amount)s BTC to %(address)s")
- payment.status_message = msg % dict(amount=str(btc_price), address=address)
- payment.backend_extid = address
- payment.backend_data = dict(btc_price=str(btc_price), btc_address=address)
- payment.save()
- return redirect(reverse('payments:view', args=(payment.id,)))
-
- def check(self, payment):
- rpc = self.make_rpc()
-
- if payment.status != 'new':
- return
-
- btc_price = payment.backend_data.get('btc_price')
- address = payment.backend_data.get('btc_address')
- if not btc_price or not address:
- return
-
- btc_price = Decimal(btc_price)
-
- received = Decimal(rpc.getreceivedbyaddress(address)) / self.COIN
- payment.paid_amount = int(received * self.btc_value)
- payment.backend_data['btc_paid_price'] = str(received)
-
- if received >= btc_price:
- payment.user.vpnuser.add_paid_time(payment.time)
- payment.user.vpnuser.on_payment_confirmed(payment)
- payment.user.vpnuser.save()
-
- payment.status = 'confirmed'
-
- payment.save()
-
- def get_info(self):
- rpc = self.make_rpc()
-
- try:
- info = rpc.getinfo()
- if not info:
- return [(_("Status"), "Error: got None")]
- except Exception as e:
- return [(_("Status"), "Error: " + repr(e))]
- v = info.get('version', 0)
- return (
- (_("Bitcoin value"), "%.2f €" % (self.btc_value / 100)),
- (_("Testnet"), info['testnet']),
- (_("Balance"), '{:f}'.format(info['balance'] / self.COIN)),
- (_("Blocks"), info['blocks']),
- (_("Bitcoind version"), '.'.join(str(v // 10 ** (2 * i) % 10 ** (2 * i))
- for i in range(3, -1, -1))),
- )
-
- def get_ext_url(self, payment):
- if not payment.backend_extid:
- return None
- return 'https://blockstream.info/address/%s' % payment.backend_extid
-
-
diff --git a/payments/backends/coinpayments.py b/payments/backends/coinpayments.py
deleted file mode 100644
index e86c5e4..0000000
--- a/payments/backends/coinpayments.py
+++ /dev/null
@@ -1,301 +0,0 @@
-import math
-from decimal import Decimal
-
-from django.shortcuts import redirect
-from django.utils.translation import ugettext_lazy as _
-from django.urls import reverse
-from constance import config as site_config
-
-from django.conf import settings as project_settings
-from .base import BackendBase
-
-
-import hmac
-import hashlib
-import requests
-import logging
-import json
-from urllib.parse import urlencode
-logger = logging.getLogger(__name__)
-
-
-class CoinPaymentsError(Exception):
- pass
-
-
-class CoinPayments:
- def __init__(self, pkey, skey, api_url=None):
- self.public_key = pkey
- self.secret_key = skey.encode('utf-8')
- self.api_url = api_url or 'https://www.coinpayments.net/api.php'
-
- def _sign(self, params):
- body = urlencode(params).encode('utf-8')
- mac = hmac.new(self.secret_key, body, hashlib.sha512)
- return body, mac.hexdigest()
-
- def _request(self, cmd, params):
- params.update({
- 'cmd': cmd,
- 'key': self.public_key,
- 'format': 'json',
- 'version': 1,
- })
- print(params)
- post_body, mac = self._sign(params)
-
- headers = {
- 'HMAC': mac,
- 'Content-Type': 'application/x-www-form-urlencoded',
- }
-
- r = requests.post(self.api_url, data=post_body,
- headers=headers)
- try:
- r.raise_for_status()
- j = r.json()
- except Exception as e:
- raise CoinPaymentsError(str(e)) from e
-
- if j.get('error') == 'ok':
- return j.get('result')
- else:
- raise CoinPaymentsError(j.get('error'))
-
- def create_transaction(self, **params):
- assert 'amount' in params
- assert 'currency1' in params
- assert 'currency2' in params
- return self._request('create_transaction', params)
-
- def get_account_info(self, **params):
- return self._request('get_basic_info', params)
-
- def get_rates(self, **params):
- return self._request('rates', params)
-
- def get_balances(self, **params):
- return self._request('balances', params)
-
- def get_deposit_address(self, **params):
- assert 'currency' in params
- return self._request('get_deposit_address', params)
-
- def get_callback_address(self, **params):
- assert 'currency' in params
- return self._request('get_callback_address', params)
-
- def get_tx_info(self, **params):
- assert 'txid' in params
- return self._request('get_tx_info', params)
-
- def get_tx_info_multi(self, ids=None, **params):
- if ids is not None:
- params['txid'] = '|'.join(str(i) for i in ids)
- assert 'txid' in params
- return self._request('get_tx_info_multi', params)
-
- def get_tx_ids(self, **params):
- return self._request('get_tx_ids', params)
-
- def create_transfer(self, **params):
- assert 'amount' in params
- assert 'currency' in params
- assert 'merchant' in params or 'pbntag' in params
- return self._request('create_transfer', params)
-
- def create_withdrawal(self, **params):
- assert 'amount' in params
- assert 'currency' in params
- assert 'address' in params or 'pbntag' in params
- return self._request('create_withdrawal', params)
-
- def create_mass_withdrawal(self, **params):
- assert 'wd' in params
- return self._request('create_mass_withdrawal', params)
-
- def convert(self, **params):
- assert 'amount' in params
- assert 'from' in params
- assert 'to' in params
- return self._request('convert', params)
-
- def get_withdrawal_history(self, **params):
- return self._request('get_withdrawal_history', params)
-
- def get_withdrawal_info(self, **params):
- assert 'id' in params
- return self._request('get_withdrawal_info', params)
-
- def get_conversion_info(self, **params):
- assert 'id' in params
- return self._request('get_conversion_info', params)
-
- def get_pbn_info(self, **params):
- assert 'pbntag' in params
- return self._request('get_pbn_info', params)
-
- def get_pbn_list(self, **params):
- return self._request('get_pbn_list', params)
-
- def update_pbn_tag(self, **params):
- assert 'tagid' in params
- return self._request('update_pbn_tag', params)
-
- def claim_pbn_tag(self, **params):
- assert 'tagid' in params
- assert 'name' in params
- return self._request('claim_pbn_tag', params)
-
-
-class IpnError(Exception):
- pass
-
-
-def ipn_assert(request, remote, local, key=None, delta=None):
- if (delta is None and remote != local) or (delta is not None and not math.isclose(remote, local, abs_tol=delta)):
- logger.debug("Invalid IPN %r: local=%r remote=%r",
- key, local, remote)
- raise IpnError("Unexpected value: %s" % key)
-
-
-def ipn_assert_post(request, key, local):
- remote = request.POST.get(key)
- ipn_assert(request, remote, local, key=key)
-
-
-class CoinPaymentsBackend(BackendBase):
- backend_id = 'coinpayments'
- backend_verbose_name = _("CoinPayments")
- backend_display_name = _("Cryptocurrencies")
- backend_has_recurring = False
-
- def __init__(self, settings):
- self.merchant_id = settings.get('merchant_id')
- self.currency = settings.get('currency', 'EUR')
- self.api_base = settings.get('api_base', None)
- self.title = settings.get('title', 'VPN Payment')
- self.secret = settings.get('secret', '').encode('utf-8')
-
- if self.merchant_id and self.secret:
- self.backend_enabled = True
-
- def new_payment(self, payment):
- ROOT_URL = project_settings.ROOT_URL
- params = {
- 'cmd': '_pay',
- 'reset': '1',
- 'want_shipping': '0',
- 'merchant': self.merchant_id,
- 'currency': self.currency,
- 'amountf': '%.2f' % (payment.amount / 100),
- 'item_name': self.title,
- 'ipn_url': ROOT_URL + reverse('payments:cb_coinpayments', args=(payment.id,)),
- 'success_url': ROOT_URL + reverse('payments:view', args=(payment.id,)),
- 'cancel_url': ROOT_URL + reverse('payments:cancel', args=(payment.id,)),
- }
-
- payment.status_message = _("Waiting for CoinPayments to confirm the transaction... " +
- "It can take up to a few minutes...")
- payment.save()
-
- form = '
-
- '''
- return form
-
- def handle_ipn(self, payment, request):
- sig = request.META.get('HTTP_HMAC')
- if not sig:
- raise IpnError("Missing HMAC")
-
- mac = hmac.new(self.secret, request.body, hashlib.sha512).hexdigest()
-
- # Sanity checks, if it fails the IPN is to be ignored
- ipn_assert(request, sig, mac, 'HMAC')
- ipn_assert_post(request, 'ipn_mode', 'hmac')
- ipn_assert_post(request, 'merchant', self.merchant_id)
-
- try:
- status = int(request.POST.get('status'))
- except ValueError:
- raise IpnError("Invalid status (%r)" % status)
-
- # Some states are final (can't cancel a timeout or refund)
- if payment.status not in ('new', 'confirmed', 'error'):
- m = "Unexpected state change for %s: is %s, received status=%r" % (
- payment.id, payment.status, status
- )
- raise IpnError(m)
-
- # whatever the status, we can safely update the text and save the tx id
- payment.status_text = request.POST.get('status_text') or payment.status_text
- payment.backend_extid = request.POST.get('txn_id')
-
- received_amount = request.POST.get('amount1')
- if received_amount:
- payment.paid_amount = float(received_amount) * 100
-
- # And now the actual processing
- if status == 1: # A payment is confirmed paid
- if payment.status != 'confirmed':
- if payment.paid_amount != payment.amount:
- ipn_assert(request, payment.paid_amount, payment.amount, 'paid',
- delta=10)
- vpnuser = payment.user.vpnuser
- vpnuser.add_paid_time(payment.time)
- vpnuser.on_payment_confirmed(payment)
- vpnuser.save()
-
- # We save the new state *at the end*
- # (it will be retried if there's an error)
- payment.status = 'confirmed'
- payment.status_message = None
- payment.save()
-
- elif status > 1: # Waiting (that's further confirmation about funds getting moved)
- # we have nothing to do, except updating status_text
- payment.save()
- return
-
- elif status == -1: # Cancel / Time out
- payment.status = 'cancelled'
- payment.save()
-
- elif status == -2: # A refund
- if payment.status == 'confirmed': # (paid -> refunded)
- payment.status = 'refunded'
- # TODO
-
- elif status <= -3: # Unknown error
- payment.status = 'error'
- payment.save()
-
- def callback(self, payment, request):
- try:
- self.handle_ipn(payment, request)
- return True
- except IpnError as e:
- payment.status = 'error'
- payment.status_message = ("Error processing the payment. "
- "Please contact support.")
- payment.backend_data['ipn_exception'] = repr(e)
- payment.backend_data['ipn_last_data'] = repr(request.POST)
- payment.save()
- logger.warn("IPN error: %s", e)
- raise
-
-
diff --git a/payments/backends/paypal.py b/payments/backends/paypal.py
deleted file mode 100644
index 91dc0fb..0000000
--- a/payments/backends/paypal.py
+++ /dev/null
@@ -1,252 +0,0 @@
-from django.shortcuts import redirect
-from django.utils.translation import ugettext_lazy as _
-from urllib.parse import urlencode
-from urllib.request import urlopen
-from django.urls import reverse
-from django.conf import settings as project_settings
-import requests
-
-from .base import BackendBase
-
-
-def urljoin(a, b):
- if b.startswith('/') and a.endswith('/'):
- return a + b[1:]
- if b.startswith('/') or a.endswith('/'):
- return a + b
- return a + "/" + b
-
-
-class PaypalBackend(BackendBase):
- backend_id = 'paypal'
- backend_verbose_name = _("PayPal")
- backend_display_name = _("PayPal")
- backend_has_recurring = True
-
- def __init__(self, settings):
- self.test = settings.get('test', False)
- self.header_image = settings.get('header_image', None)
- self.title = settings.get('title', 'VPN Payment')
- self.currency = settings.get('currency', 'EUR')
- self.account_address = settings.get('address')
- self.receiver_address = settings.get('receiver', self.account_address)
-
- self.api_username = settings.get('api_username')
- self.api_password = settings.get('api_password')
- self.api_sig = settings.get('api_sig')
-
- if self.test:
- default_nvp = 'https://api-3t.sandbox.paypal.com/nvp'
- default_api = 'https://www.sandbox.paypal.com/'
- else:
- default_nvp = 'https://api-3t.paypal.com/nvp'
- default_api = 'https://www.paypal.com/'
- self.api_base = settings.get('api_base', default_api)
- self.nvp_api_base = settings.get('nvp_api_base', default_nvp)
-
- if self.account_address and self.api_username and self.api_password and self.api_sig:
- self.backend_enabled = True
-
- def new_payment(self, payment):
- ROOT_URL = project_settings.ROOT_URL
- params = {
- 'cmd': '_xclick',
- 'notify_url': ROOT_URL + reverse('payments:cb_paypal', args=(payment.id,)),
- 'item_name': self.title,
- 'amount': '%.2f' % (payment.amount / 100),
- 'currency_code': self.currency,
- 'business': self.account_address,
- 'no_shipping': '1',
- 'return': ROOT_URL + reverse('payments:view', args=(payment.id,)),
- 'cancel_return': ROOT_URL + reverse('payments:cancel', args=(payment.id,)),
- }
-
- if self.header_image:
- params['cpp_header_image'] = self.header_image
-
- payment.status_message = _("Waiting for PayPal to confirm the transaction... " +
- "It can take up to a few minutes...")
- payment.save()
-
- return redirect(urljoin(self.api_base, '/cgi-bin/webscr?' + urlencode(params)))
-
- def new_subscription(self, rps):
- months = {
- '3m': 3,
- '6m': 6,
- '12m': 12,
- }[rps.period]
-
- ROOT_URL = project_settings.ROOT_URL
- params = {
- 'cmd': '_xclick-subscriptions',
- 'notify_url': ROOT_URL + reverse('payments:cb_paypal_subscr', args=(rps.id,)),
- 'item_name': self.title,
- 'currency_code': self.currency,
- 'business': self.account_address,
- 'no_shipping': '1',
- 'return': ROOT_URL + reverse('payments:return_subscr', args=(rps.id,)),
- 'cancel_return': ROOT_URL + reverse('account:index'),
-
- 'a3': '%.2f' % (rps.period_amount / 100),
- 'p3': str(months),
- 't3': 'M',
- 'src': '1',
- }
-
- if self.header_image:
- params['cpp_header_image'] = self.header_image
-
- rps.save()
-
- return redirect(urljoin(self.api_base, '/cgi-bin/webscr?' + urlencode(params)))
-
- def handle_verified_callback(self, payment, params):
- if self.test and params['test_ipn'] != '1':
- raise ValueError('Test IPN')
-
- txn_type = params.get('txn_type')
- if txn_type not in (None, 'web_accept', 'express_checkout'):
- # Not handled here and can be ignored
- return
-
- if params['payment_status'] == 'Refunded':
- payment.status = 'refunded'
- payment.status_message = None
-
- elif params['payment_status'] == 'Completed':
- self.handle_completed_payment(payment, params)
-
- def handle_verified_callback_subscr(self, subscr, params):
- if self.test and params['test_ipn'] != '1':
- raise ValueError('Test IPN')
-
- txn_type = params.get('txn_type')
- if not txn_type.startswith('subscr_'):
- # Not handled here and can be ignored
- return
-
- if txn_type == 'subscr_payment':
- if params['payment_status'] == 'Refunded':
- # FIXME: Find the payment and do something
- pass
-
- elif params['payment_status'] == 'Completed':
- payment = subscr.create_payment()
- if not self.handle_completed_payment(payment, params):
- return
-
- subscr.last_confirmed_payment = payment.created
- subscr.backend_extid = params.get('subscr_id', '')
- if subscr.status == 'new' or subscr.status == 'unconfirmed':
- subscr.status = 'active'
- subscr.save()
- elif txn_type == 'subscr_cancel' or txn_type == 'subscr_eot':
- subscr.status = 'cancelled'
- subscr.save()
-
- def handle_completed_payment(self, payment, params):
- from payments.models import Payment
-
- # Prevent making duplicate Payments if IPN is received twice
- pc = Payment.objects.filter(backend_extid=params['txn_id']).count()
- if pc > 0:
- return False
-
- if self.receiver_address != params['receiver_email']:
- raise ValueError('Wrong receiver: ' + params['receiver_email'])
- if self.currency.lower() != params['mc_currency'].lower():
- raise ValueError('Wrong currency: ' + params['mc_currency'])
-
- payment.paid_amount = int(float(params['mc_gross']) * 100)
- if payment.paid_amount < payment.amount:
- raise ValueError('Not fully paid.')
-
- payment.user.vpnuser.add_paid_time(payment.time)
- payment.user.vpnuser.on_payment_confirmed(payment)
- payment.user.vpnuser.save()
-
- payment.backend_extid = params['txn_id']
- payment.status = 'confirmed'
- payment.status_message = None
- payment.save()
-
- payment.user.vpnuser.lcore_sync()
- return True
-
- def verify_ipn(self, request):
- v_url = urljoin(self.api_base, '/cgi-bin/webscr?cmd=_notify-validate')
- v_req = urlopen(v_url, data=request.body, timeout=5)
- v_res = v_req.read()
- return v_res == b'VERIFIED'
-
- def callback(self, payment, request):
- if not self.verify_ipn(request):
- return False
-
- params = request.POST
-
- try:
- self.handle_verified_callback(payment, params)
- return True
- except (KeyError, ValueError) as e:
- payment.status = 'error'
- payment.status_message = None
- payment.backend_data['ipn_exception'] = repr(e)
- payment.backend_data['ipn_last_data'] = repr(request.POST)
- payment.save()
- raise
-
- def callback_subscr(self, subscr, request):
- if not self.verify_ipn(request):
- return False
-
- params = request.POST
-
- try:
- self.handle_verified_callback_subscr(subscr, params)
- return True
- except (KeyError, ValueError) as e:
- subscr.status = 'error'
- subscr.status_message = None
- subscr.backend_data['ipn_exception'] = repr(e)
- subscr.backend_data['ipn_last_data'] = repr(request.POST)
- subscr.save()
- raise
-
- def cancel_subscription(self, subscr):
- if not subscr.backend_extid:
- return False
-
- try:
- r = requests.post(self.nvp_api_base, data={
- "METHOD": "ManageRecurringPaymentsProfileStatus",
- "PROFILEID": subscr.backend_extid,
- "ACTION": "cancel",
- "USER": self.api_username,
- "PWD": self.api_password,
- "SIGNATURE": self.api_sig,
- "VERSION": "204.0",
- })
- r.raise_for_status()
- print(r.text)
-
- subscr.status = 'cancelled'
- subscr.save()
- return True
- except Exception as e:
- print(e)
- return False
-
- def get_ext_url(self, payment):
- if not payment.backend_extid:
- return None
- url = 'https://history.paypal.com/webscr?cmd=_history-details-from-hub&id=%s'
- return url % payment.backend_extid
-
- def get_subscr_ext_url(self, subscr):
- if not subscr.backend_extid:
- return None
- return ('https://www.paypal.com/fr/cgi-bin/webscr?cmd=_profile-recurring-payments'
- '&encrypted_profile_id=%s' % subscr.backend_extid)
-
diff --git a/payments/backends/stripe.py b/payments/backends/stripe.py
deleted file mode 100644
index 7311c51..0000000
--- a/payments/backends/stripe.py
+++ /dev/null
@@ -1,304 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-from django.urls import reverse
-from django.conf import settings as project_settings
-
-from .base import BackendBase
-
-
-class StripeBackend(BackendBase):
- backend_id = 'stripe'
- backend_verbose_name = _("Stripe")
- backend_display_name = _("Credit Card")
- backend_has_recurring = True
-
- def get_plan_id(self, period):
- return 'ccvpn_' + period
-
- def __init__(self, settings):
- self.public_key = settings.get('public_key')
- self.secret_key = settings.get('secret_key')
- self.wh_key = settings.get('wh_key')
-
- if not self.public_key or not self.secret_key or not self.wh_key:
- raise Exception("Missing keys for stripe backend")
-
- import stripe
- self.stripe = stripe
- stripe.api_key = self.secret_key
-
- self.header_image = settings.get('header_image', '')
- self.currency = settings.get('currency', 'EUR')
- self.title = settings.get('title', 'VPN Payment')
- self.backend_enabled = True
-
- def make_redirect(self, session):
- return '''
-
-
-
- '''.format(pk=self.public_key, sess=session['id'])
-
- def new_payment(self, payment):
- root_url = project_settings.ROOT_URL
- assert root_url
-
- months = payment.time.days // 30
- if months > 1:
- desc = '{} months for {}'.format(months, payment.user.username)
- else:
- desc = 'One month for {}'.format(payment.user.username)
-
- session = self.stripe.checkout.Session.create(
- success_url=root_url + reverse('payments:view', args=(payment.id,)),
- cancel_url=root_url + reverse('payments:cancel', args=(payment.id,)),
- payment_method_types=['card'],
- line_items=[
- {
- 'amount': payment.amount,
- 'currency': self.currency.lower(),
- 'name': self.title,
- 'description': desc,
- 'quantity': 1,
- }
- ],
- )
- payment.backend_extid = session['id']
- payment.backend_data = {'session_id': session['id']}
- payment.save()
- return self.make_redirect(session)
-
- def new_subscription(self, subscr):
- root_url = project_settings.ROOT_URL
- assert root_url
-
- session = self.stripe.checkout.Session.create(
- success_url=root_url + reverse('payments:return_subscr', args=(subscr.id,)),
- cancel_url=root_url + reverse('payments:cancel_subscr', args=(subscr.id,)),
- client_reference_id='sub_%d'%subscr.id,
- payment_method_types=['card'],
- subscription_data={
- 'items': [{
- 'plan': self.get_plan_id(subscr.period),
- 'quantity': 1,
- }],
- },
- )
- subscr.backend_data = {'session_id': session['id']}
- subscr.save()
- return self.make_redirect(session)
-
- def cancel_subscription(self, subscr):
- if subscr.status not in ('new', 'unconfirmed', 'active'):
- return
-
- if subscr.backend_extid.startswith('pi'):
- # a session that didn't create a subscription yet (intent)
- pass
- elif subscr.backend_extid.startswith('sub_'):
- # Subscription object
- try:
- self.stripe.Subscription.delete(subscr.backend_extid)
- except self.stripe.error.InvalidRequestError:
- pass
- elif subscr.backend_extid.startswith('cus_'):
- # Legacy Customer object
- try:
- cust = self.stripe.Customer.retrieve(subscr.backend_extid)
- except self.stripe.error.InvalidRequestError:
- return
-
- try:
- # Delete customer and cancel any active subscription
- cust.delete()
- except self.stripe.error.InvalidRequestError:
- pass
- else:
- raise Exception("Failed to cancel subscription %r" % subscr.backend_extid)
-
- subscr.status = 'cancelled'
- subscr.save()
- return True
-
- def refresh_subscription(self, subscr):
- if subscr.backend_extid.startswith('cus_'):
- customer = self.stripe.Customer.retrieve(subscr.backend_extid)
- for s in customer['subscriptions']['data']:
- if s['status'] == 'active':
- sub = s
- break
- else:
- return
- elif subscr.backend_extid.startswith('sub_'):
- sub = self.stripe.Subscription.retrieve(subscr.backend_extid)
- else:
- print("unhandled subscription backend extid: {}".format(subscr.backend_extid))
- return
-
- if sub['status'] == 'canceled':
- subscr.status = 'cancelled'
- if sub['status'] == 'past_due':
- subscr.status = 'error'
-
- def webhook_session_completed(self, event):
- session = event['data']['object']
-
- if session['subscription']:
- # Subscription creation
- from payments.models import Payment, Subscription
-
- sub_id = session['subscription']
- assert sub_id
-
- parts = session['client_reference_id'].split('_')
- if len(parts) != 2 or parts[0] != 'sub':
- raise Exception("invalid reference id")
- sub_internal_id = int(parts[1])
-
- # Fetch sub by ID and confirm it
- subscr = Subscription.objects.get(id=sub_internal_id)
- subscr.status = 'active'
- subscr.backend_extid = sub_id
- subscr.set_data('subscription_id', sub_id)
- subscr.save()
-
- else:
- from payments.models import Payment
- payment = Payment.objects.filter(backend_extid=session['id']).get()
-
- # the amount is provided server-side, we do not have to check
- payment.paid_amount = payment.amount
-
- payment.status = 'confirmed'
- payment.status_message = None
- payment.save()
- payment.user.vpnuser.add_paid_time(payment.time)
- payment.user.vpnuser.on_payment_confirmed(payment)
- payment.user.vpnuser.save()
-
- payment.user.vpnuser.lcore_sync()
-
- def get_subscription_from_invoice(self, invoice):
- from payments.models import Subscription
-
- subscription_id = invoice['subscription']
- customer_id = invoice['customer']
-
- # once it's confirmed, the id to the subscription is stored as extid
- subscr = Subscription.objects.filter(backend_extid=subscription_id).first()
- if subscr:
- return subscr
-
- # older subscriptions will have a customer id instead
- subscr = Subscription.objects.filter(backend_extid=customer_id).first()
- if subscr:
- return subscr
-
- return None
-
- def webhook_payment_succeeded(self, event):
- """ webhook event for a subscription's succeeded payment """
- from payments.models import Payment
-
- invoice = event['data']['object']
- subscr = self.get_subscription_from_invoice(invoice)
- if not subscr:
- # the subscription does not exist
- # checkout.confirmed event will create it and handle the initial payment
- # return True
- raise Exception("Unknown subscription for invoice %r" % invoice['id'])
-
- # Prevent making duplicate Payments if event is received twice
- pc = Payment.objects.filter(backend_extid=invoice['id']).count()
- if pc > 0:
- return
-
- payment = subscr.create_payment()
- payment.status = 'confirmed'
- payment.paid_amount = payment.amount
- payment.backend_extid = invoice['id']
- if invoice['subscription']:
- if isinstance(invoice['subscription'], str):
- payment.backend_sub_id = invoice['subscription']
- else:
- payment.backend_sub_id = invoice['subscription']['id']
- payment.set_data('event_id', event['id'])
- payment.set_data('sub_id', payment.backend_sub_id)
- payment.save()
-
- payment.user.vpnuser.add_paid_time(payment.time)
- payment.user.vpnuser.on_payment_confirmed(payment)
- payment.user.vpnuser.save()
- payment.save()
-
- payment.user.vpnuser.lcore_sync()
-
- def webhook_subscr_update(self, event):
- from payments.models import Subscription
- stripe_sub = event['data']['object']
- sub = Subscription.objects.get(backend_id='stripe', backend_extid=stripe_sub['id'])
-
- if not sub:
- return
-
- if stripe_sub['status'] == 'canceled':
- sub.status = 'cancelled'
- if stripe_sub['status'] == 'past_due':
- sub.status = 'error'
- sub.save()
-
- def webhook(self, request):
- payload = request.body
- sig_header = request.META['HTTP_STRIPE_SIGNATURE']
-
- try:
- event = self.stripe.Webhook.construct_event(
- payload, sig_header, self.wh_key,
- )
- except (ValueError, self.stripe.error.InvalidRequestError, self.stripe.error.SignatureVerificationError):
- return False
-
- if event['type'] == 'invoice.payment_succeeded':
- self.webhook_payment_succeeded(event)
- if event['type'] == 'checkout.session.completed':
- self.webhook_session_completed(event)
- if event['type'] == 'customer.subscription.deleted':
- self.webhook_subscr_update(event)
- return True
-
- def get_ext_url(self, payment):
- extid = payment.backend_extid
-
- if not extid:
- return None
-
- if extid.startswith('in_'):
- return 'https://dashboard.stripe.com/invoices/%s' % extid
- if extid.startswith('ch_'):
- return 'https://dashboard.stripe.com/payments/%s' % extid
-
- def get_subscr_ext_url(self, subscr):
- extid = subscr.backend_extid
-
- if not extid:
- return None
-
- if extid.startswith('sub_') and self.stripe:
- livemode = False
- try:
- sub = self.stripe.Subscription.retrieve(extid)
- livemode = sub['livemode']
- except Exception:
- pass
- if livemode:
- return 'https://dashboard.stripe.com/subscriptions/' + extid
- else:
- return 'https://dashboard.stripe.com/test/subscriptions/' + extid
-
- if extid.startswith('cus_'):
- return 'https://dashboard.stripe.com/customers/%s' % subscr.backend_extid
diff --git a/payments/forms.py b/payments/forms.py
deleted file mode 100644
index b2245a5..0000000
--- a/payments/forms.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django import forms
-from .models import BACKEND_CHOICES
-
-
-class NewPaymentForm(forms.Form):
- TIME_CHOICES = (
- ('1', '1'),
- ('3', '3'),
- ('6', '6'),
- ('12', '12'),
- )
-
- subscr = forms.ChoiceField(choices=(('0', 'no'), ('1', 'yes')))
- time = forms.ChoiceField(choices=TIME_CHOICES)
- method = forms.ChoiceField(choices=BACKEND_CHOICES)
-
diff --git a/payments/management/__init__.py b/payments/management/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/payments/management/commands/__init__.py b/payments/management/commands/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/payments/management/commands/bitcoin_info.py b/payments/management/commands/bitcoin_info.py
deleted file mode 100644
index 66265fd..0000000
--- a/payments/management/commands/bitcoin_info.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-
-from payments.models import ACTIVE_BACKENDS
-
-
-class Command(BaseCommand):
- help = "Get bitcoind info"
-
- def handle(self, *args, **options):
- if 'bitcoin' not in ACTIVE_BACKENDS:
- raise CommandError("bitcoin backend not active.")
-
- backend = ACTIVE_BACKENDS['bitcoin']
- for key, value in backend.get_info():
- self.stdout.write("%s: %s" % (key, value))
diff --git a/payments/management/commands/check_btc_payments.py b/payments/management/commands/check_btc_payments.py
deleted file mode 100644
index 0a0e68b..0000000
--- a/payments/management/commands/check_btc_payments.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-
-from payments.models import Payment, ACTIVE_BACKENDS
-
-
-class Command(BaseCommand):
- help = "Check bitcoin payments status"
-
- def handle(self, *args, **options):
- if 'bitcoin' not in ACTIVE_BACKENDS:
- raise CommandError("bitcoin backend not active.")
-
- backend = ACTIVE_BACKENDS['bitcoin']
-
- payments = Payment.objects.filter(backend_id='bitcoin', status='new')
-
- self.stdout.write("Found %d active unconfirmed payments." % len(payments))
-
- for p in payments:
- self.stdout.write("Checking payment #%d... " % p.id, ending="")
- backend.check(p)
-
- if p.status == 'confirmed':
- self.stdout.write("OK.")
- else:
- self.stdout.write("Waiting")
-
-
diff --git a/payments/management/commands/confirm_payment.py b/payments/management/commands/confirm_payment.py
deleted file mode 100644
index 9cbbe4a..0000000
--- a/payments/management/commands/confirm_payment.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from django.utils.dateparse import parse_duration
-
-from payments.models import Payment
-
-
-class Command(BaseCommand):
- help = "Manually confirm a Payment"
-
- def add_arguments(self, parser):
- parser.add_argument('id', action='store', type=int, help="Payment ID")
- parser.add_argument('--paid-amount', dest='amount', action='store', type=int, help="Paid amount")
- parser.add_argument('--extid', dest='extid', action='store', type=str)
- parser.add_argument('-n', dest='sim', action='store_true', help="Simulate")
-
- def handle(self, *args, **options):
- try:
- p = Payment.objects.get(id=options['id'])
- except Payment.DoesNotExist:
- self.stderr.write("Cannot find payment #%d" % options['id'])
- return
-
- print("Payment #%d by %s (amount=%d; paid_amount=%d)" % (p.id, p.user.username, p.amount, p.paid_amount))
-
- if options['amount']:
- pa = options['amount']
- else:
- pa = p.amount
-
- extid = options['extid']
-
- print("Status -> confirmed")
- print("Paid amount -> %d" % pa)
- if extid:
- print("Ext ID -> %s" % extid)
-
- print("Confirm? [y/n] ")
- i = input()
- if i.lower().strip() == 'y':
- p.user.vpnuser.add_paid_time(p.time)
- p.user.vpnuser.on_payment_confirmed(p)
- p.user.vpnuser.save()
-
- p.paid_amount = pa
- p.status = 'confirmed'
- if extid:
- p.backend_extid = extid
- p.save()
- else:
- print("aborted.")
-
diff --git a/payments/management/commands/expire_payments.py b/payments/management/commands/expire_payments.py
deleted file mode 100644
index deaf1e0..0000000
--- a/payments/management/commands/expire_payments.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.core.management.base import BaseCommand
-from django.utils import timezone
-from django.utils.dateparse import parse_duration
-
-from payments.models import Payment
-
-
-class Command(BaseCommand):
- help = "Cancels expired Payments"
-
- def add_arguments(self, parser):
- parser.add_argument('-n', dest='sim', action='store_true', help="Simulate")
- parser.add_argument('-e', '--exp-time', action='store',
- help="Expiration time.", default='3 00:00:00')
-
- def handle(self, *args, **options):
- now = timezone.now()
- expdate = now - parse_duration(options['exp_time'])
-
- self.stdout.write("Now: " + now.isoformat())
- self.stdout.write("Exp: " + expdate.isoformat())
-
- expired = Payment.objects.filter(created__lte=expdate, status='new',
- paid_amount=0)
-
- for p in expired:
- self.stdout.write("Payment #%d (%s): %s" % (p.id, p.user.username, p.created))
- if not options['sim']:
- p.status = 'cancelled'
- p.save()
-
diff --git a/payments/management/commands/update_stripe_plans.py b/payments/management/commands/update_stripe_plans.py
deleted file mode 100644
index 444dc69..0000000
--- a/payments/management/commands/update_stripe_plans.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from django.core.management.base import BaseCommand, CommandError
-from django.conf import settings
-
-from ccvpn.common import get_price
-from payments.models import ACTIVE_BACKENDS, SUBSCR_PERIOD_CHOICES, period_months
-
-CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY
-
-
-class Command(BaseCommand):
- help = "Update Stripe plans"
-
- def add_arguments(self, parser):
- parser.add_argument('--force-run', action='store_true',
- help="Run even when Stripe backend is disabled")
- parser.add_argument('--force-update', action='store_true',
- help="Replace plans, including matching ones")
-
- def handle(self, *args, **options):
- if 'stripe' not in ACTIVE_BACKENDS and options['force-run'] is False:
- raise CommandError("stripe backend not active.")
-
- backend = ACTIVE_BACKENDS['stripe']
- stripe = backend.stripe
-
- for period_id, period_name in SUBSCR_PERIOD_CHOICES:
- plan_id = backend.get_plan_id(period_id)
- months = period_months(period_id)
- amount = months * get_price()
-
- kwargs = dict(
- id=plan_id,
- amount=amount,
- interval='month',
- interval_count=months,
- name="VPN Subscription (%s)" % period_id,
- currency=CURRENCY_CODE,
- )
-
- self.stdout.write('Plan %s: %d months for %.2f %s (%s)... ' % (
- plan_id, months, amount / 100, CURRENCY_NAME, CURRENCY_CODE), ending='')
- self.stdout.flush()
-
- try:
- plan = stripe.Plan.retrieve(plan_id)
- except stripe.error.InvalidRequestError:
- plan = None
-
- def is_valid_plan():
- if not plan:
- return False
- for k, v in kwargs.items():
- if getattr(plan, k) != v:
- return False
- return True
-
- if plan:
- if is_valid_plan() and not options['force_update']:
- self.stdout.write(self.style.SUCCESS('[ok]'))
- continue
- plan.delete()
- update = True
- else:
- update = False
-
- stripe.Plan.create(**kwargs)
- if update:
- self.stdout.write(self.style.WARNING('[updated]'))
- else:
- self.stdout.write(self.style.WARNING('[created]'))
diff --git a/payments/migrations/0001_initial.py b/payments/migrations/0001_initial.py
deleted file mode 100644
index 3425c63..0000000
--- a/payments/migrations/0001_initial.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-import jsonfield.fields
-from django.conf import settings
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Payment',
- fields=[
- ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
- ('backend_id', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)),
- ('status', models.CharField(choices=[('new', 'Waiting for payment'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('rejected', 'Rejected by processor'), ('error', 'Payment processing failed')], max_length=16)),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('modified', models.DateTimeField(auto_now=True)),
- ('confirmed_on', models.DateTimeField(null=True, blank=True)),
- ('amount', models.IntegerField()),
- ('paid_amount', models.IntegerField(default=0)),
- ('time', models.DurationField()),
- ('status_message', models.TextField(null=True, blank=True)),
- ('backend_extid', models.CharField(null=True, max_length=64, blank=True)),
- ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)),
- ],
- options={
- 'ordering': ('-created',),
- },
- ),
- migrations.CreateModel(
- name='RecurringPaymentSource',
- fields=[
- ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)),
- ('backend', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('modified', models.DateTimeField(auto_now=True)),
- ('period', models.CharField(choices=[('monthly', 'Monthly'), ('biannually', 'Bianually'), ('yearly', 'Yearly')], max_length=16)),
- ('last_confirmed_payment', models.DateTimeField(null=True, blank=True)),
- ('backend_id', models.CharField(null=True, max_length=64, blank=True)),
- ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)),
- ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
- ],
- ),
- migrations.AddField(
- model_name='payment',
- name='recurring_source',
- field=models.ForeignKey(null=True, to='payments.RecurringPaymentSource', blank=True, on_delete=models.CASCADE),
- ),
- migrations.AddField(
- model_name='payment',
- name='user',
- field=models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL),
- ),
- ]
diff --git a/payments/migrations/0002_auto_20151204_0341.py b/payments/migrations/0002_auto_20151204_0341.py
deleted file mode 100644
index 8755270..0000000
--- a/payments/migrations/0002_auto_20151204_0341.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('payments', '0001_initial'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='recurringpaymentsource',
- name='period',
- field=models.CharField(max_length=16, choices=[('6m', 'Every 6 months'), ('1year', 'Yearly')]),
- ),
- ]
diff --git a/payments/migrations/0003_auto_20151209_0440.py b/payments/migrations/0003_auto_20151209_0440.py
deleted file mode 100644
index 312fb3f..0000000
--- a/payments/migrations/0003_auto_20151209_0440.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9 on 2015-12-09 04:40
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('payments', '0002_auto_20151204_0341'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='payment',
- name='status',
- field=models.CharField(choices=[('new', 'Waiting for payment'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('rejected', 'Rejected by processor'), ('error', 'Payment processing failed')], default='new', max_length=16),
- ),
- ]
diff --git a/payments/migrations/0004_auto_20160904_0048.py b/payments/migrations/0004_auto_20160904_0048.py
deleted file mode 100644
index c7db118..0000000
--- a/payments/migrations/0004_auto_20160904_0048.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.5 on 2016-09-04 00:48
-from __future__ import unicode_literals
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import jsonfield.fields
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('payments', '0003_auto_20151209_0440'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Subscription',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('backend_id', models.CharField(choices=[('bitcoin', 'Bitcoin'), ('coinbase', 'Coinbase'), ('manual', 'Manual'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16)),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('period', models.CharField(choices=[('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every year')], max_length=16)),
- ('last_confirmed_payment', models.DateTimeField(blank=True, null=True)),
- ('status', models.CharField(choices=[('new', 'Waiting for payment'), ('active', 'Active'), ('cancelled', 'Cancelled')], default='new', max_length=16)),
- ('backend_extid', models.CharField(blank=True, max_length=64, null=True)),
- ('backend_data', jsonfield.fields.JSONField(blank=True, default=dict)),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- ),
- migrations.RemoveField(
- model_name='recurringpaymentsource',
- name='user',
- ),
- migrations.RemoveField(
- model_name='payment',
- name='recurring_source',
- ),
- migrations.DeleteModel(
- name='RecurringPaymentSource',
- ),
- migrations.AddField(
- model_name='payment',
- name='subscription',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='payments.Subscription'),
- ),
- ]
diff --git a/payments/migrations/0005_auto_20160907_0018.py b/payments/migrations/0005_auto_20160907_0018.py
deleted file mode 100644
index 93d608c..0000000
--- a/payments/migrations/0005_auto_20160907_0018.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Generated by Django 1.9.5 on 2016-09-07 00:18
-from __future__ import unicode_literals
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('payments', '0004_auto_20160904_0048'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='subscription',
- name='status',
- field=models.CharField(choices=[('new', 'Created'), ('unconfirmed', 'Waiting for payment'), ('active', 'Active'), ('cancelled', 'Cancelled'), ('error', 'Error')], default='new', max_length=16),
- ),
- ]
diff --git a/payments/migrations/0006_auto_20190907_2029.py b/payments/migrations/0006_auto_20190907_2029.py
deleted file mode 100644
index d19c545..0000000
--- a/payments/migrations/0006_auto_20190907_2029.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Generated by Django 2.2.1 on 2019-09-07 20:29
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('payments', '0005_auto_20160907_0018'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='Feedback',
- fields=[
- ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('message', models.TextField()),
- ('subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='payments.Subscription')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
- ],
- ),
- ]
diff --git a/payments/migrations/0007_auto_20201114_1730.py b/payments/migrations/0007_auto_20201114_1730.py
deleted file mode 100644
index a275da1..0000000
--- a/payments/migrations/0007_auto_20201114_1730.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Generated by Django 3.1.2 on 2020-11-14 17:30
-
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('payments', '0006_auto_20190907_2029'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='payment',
- name='backend_extid',
- field=models.CharField(blank=True, max_length=256, null=True),
- ),
- migrations.AlterField(
- model_name='subscription',
- name='backend_extid',
- field=models.CharField(blank=True, max_length=256, null=True),
- ),
- ]
diff --git a/payments/migrations/0008_auto_20210721_1931.py b/payments/migrations/0008_auto_20210721_1931.py
deleted file mode 100644
index c00c579..0000000
--- a/payments/migrations/0008_auto_20210721_1931.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Generated by Django 3.2.4 on 2021-07-21 19:31
-
-from datetime import timedelta
-from django.conf import settings
-from django.db import migrations, models
-import django.db.models.deletion
-import jsonfield.fields
-
-def field_to_plan_id(model_name, field_name):
- def fun(apps, schema_editor):
- keys = settings.PLANS.keys()
- model = apps.get_model('payments', model_name)
- for s in model.objects.all():
- d = getattr(s, field_name)
- if s.plan_id is not None:
- continue
- s.plan_id = str(round(d / timedelta(days=30))) + 'm'
- if s.plan_id not in keys:
- raise Exception(f"unknown plan: {s.plan_id}")
- s.save()
- return fun
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ('payments', '0007_auto_20201114_1730'),
- ]
-
- operations = [
- migrations.RenameField("Subscription", 'period', 'plan_id'),
-
- # Add plan_id to payments and convert from the time field
- migrations.AddField(
- model_name='payment',
- name='plan_id',
- field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16, null=True),
- ),
- migrations.RunPython(field_to_plan_id('Payment', 'time'), lambda x, y: ()),
-
- # Make those two columns non-null once converted
- migrations.AlterField(
- model_name='payment',
- name='plan_id',
- field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16),
- ),
- migrations.AlterField(
- model_name='subscription',
- name='plan_id',
- field=models.CharField(choices=[('1m', 'Every 1 month'), ('3m', 'Every 3 months'), ('6m', 'Every 6 months'), ('12m', 'Every 12 months')], max_length=16),
- ),
-
- migrations.AddField(
- model_name='payment',
- name='ip_address',
- field=models.GenericIPAddressField(blank=True, null=True),
- ),
- migrations.AddField(
- model_name='payment',
- name='refund_date',
- field=models.DateTimeField(blank=True, null=True),
- ),
- migrations.AddField(
- model_name='payment',
- name='refund_text',
- field=models.CharField(blank=True, max_length=200),
- ),
- migrations.AddField(
- model_name='subscription',
- name='next_payment_date',
- field=models.DateTimeField(blank=True, null=True),
- ),
- migrations.AlterField(
- model_name='payment',
- name='backend_data',
- field=jsonfield.fields.JSONField(blank=True),
- ),
- migrations.AlterField(
- model_name='payment',
- name='backend_id',
- field=models.CharField(choices=[('bitcoin', 'Bitcoin'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16),
- ),
- migrations.AlterField(
- model_name='payment',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
- ),
- migrations.AlterField(
- model_name='subscription',
- name='backend_data',
- field=jsonfield.fields.JSONField(blank=True),
- ),
- migrations.AlterField(
- model_name='subscription',
- name='backend_id',
- field=models.CharField(choices=[('bitcoin', 'Bitcoin'), ('paypal', 'PayPal'), ('stripe', 'Stripe')], max_length=16),
- ),
- ]
diff --git a/payments/migrations/__init__.py b/payments/migrations/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/payments/models.py b/payments/models.py
deleted file mode 100644
index 5606389..0000000
--- a/payments/models.py
+++ /dev/null
@@ -1,272 +0,0 @@
-import logging
-from django.db import models
-from django.conf import settings
-from django.utils.translation import ugettext_lazy as _
-from jsonfield import JSONField
-from datetime import timedelta
-import json
-
-from ccvpn.common import get_price
-from .backends import BackendBase
-
-logger = logging.getLogger(__name__)
-
-backends_settings = settings.PAYMENTS_BACKENDS
-assert isinstance(backends_settings, dict)
-
-CURRENCY_CODE, CURRENCY_NAME = settings.PAYMENTS_CURRENCY
-
-STATUS_CHOICES = (
- ('new', _("Waiting for payment")),
- ('confirmed', _("Confirmed")),
- ('cancelled', _("Cancelled")),
- ('rejected', _("Rejected by processor")),
- ('error', _("Payment processing failed")),
-)
-
-# A Subscription is created with status='new'. When getting back from PayPal,
-# it may get upgraded to 'unconfirmed'. It will be set 'active' with the first
-# confirmed payment.
-# 'unconfirmed' exists to prevent creation of a second Subscription while
-# waiting for the first one to be confirmed.
-SUBSCR_STATUS_CHOICES = (
- ('new', _("Created")),
- ('unconfirmed', _("Waiting for payment")),
- ('active', _("Active")),
- ('cancelled', _("Cancelled")),
- ('error', _("Error")),
-)
-
-SUBSCR_PERIOD_CHOICES = (
- ('3m', _("Every 3 months")),
- ('6m', _("Every 6 months")),
- ('12m', _("Every year")),
-)
-
-BACKEND_CLASSES = BackendBase.__subclasses__()
-
-# All known backends (classes)
-BACKENDS = {}
-BACKEND_CHOICES = []
-
-# All enabled backends (configured instances)
-ACTIVE_BACKENDS = {}
-ACTIVE_BACKEND_CHOICES = []
-
-logger.info("loading payment backends...")
-for cls in BACKEND_CLASSES:
- name = cls.backend_id
- assert isinstance(name, str)
-
- if name not in backends_settings:
- logger.info("payments: ☐ %s disabled (no settings)", name)
- continue
-
- backend_settings = backends_settings.get(name, {})
- for k, v in backend_settings.items():
- if hasattr(v, '__call__'):
- backend_settings[k] = v()
-
- if not backend_settings.get('enabled'):
- logger.info("payments: ☐ %s disabled (by settings)", name)
- continue
-
- obj = cls(backend_settings)
-
- BACKENDS[name] = obj
- BACKEND_CHOICES.append((name, cls.backend_verbose_name))
-
- if obj.backend_enabled:
- ACTIVE_BACKENDS[name] = obj
- ACTIVE_BACKEND_CHOICES.append((name, cls.backend_verbose_name))
- logger.info("payments: ☑ %s initialized", name)
- else:
- logger.info("payments: ☒ %s disabled (initialization failed)", name)
-
-BACKEND_CHOICES = sorted(BACKEND_CHOICES, key=lambda x: x[0])
-ACTIVE_BACKEND_CHOICES = sorted(ACTIVE_BACKEND_CHOICES, key=lambda x: x[0])
-
-logger.info("payments: finished. %d/%d backends active", len(ACTIVE_BACKENDS), len(BACKEND_CLASSES))
-
-
-class PlanBase(object):
- def __init__(self, name, months, monthly, saves="", default=False):
- self.name = name
- self.months = months
- self.monthly = monthly
- self.saves = saves
- self.default = default
- @property
- def due_amount(self):
- return round(self.months * self.monthly, 2)
- @property
- def time_display(self):
- return "%d month%s" % (self.months, 's' if self.months > 1 else '')
- def json(self):
- return {'total': self.due_amount}
-
-
-PLANS = {k: PlanBase(name=k, **settings.PLANS[k]) for k, v in settings.PLANS.items()}
-PLAN_CHOICES = [(k, "Every " + p.time_display) for k, p in PLANS.items()]
-SUBSCR_PLAN_CHOICES = PLAN_CHOICES
-
-def period_months(p):
- return {
- '3m': 3,
- '6m': 6,
- '12m': 12,
- }[p]
-
-
-class BackendData:
- backend_data = None
-
- def set_data(self, key, value):
- """ adds a backend data key to this instance's dict """
- if not self.backend_data:
- self.backend_data = {}
-
- if isinstance(self.backend_data, str):
- self.backend_data = json.loads(self.backend_data) or {}
-
- if not isinstance(self.backend_data, dict):
- raise Exception("self.backend_data is not a dict (%r)" % self.backend_data)
- self.backend_data[key] = value
-
-
-class Payment(models.Model, BackendData):
- """ Just a payment.
- If subscription is not null, it has been automatically issued.
- backend_extid is the external transaction ID, backend_data is other
- things that should only be used by the associated backend.
- """
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- backend_id = models.CharField(max_length=16, choices=BACKEND_CHOICES)
- status = models.CharField(max_length=16, choices=STATUS_CHOICES, default='new')
- created = models.DateTimeField(auto_now_add=True)
- modified = models.DateTimeField(auto_now=True)
- confirmed_on = models.DateTimeField(null=True, blank=True)
- amount = models.IntegerField()
- paid_amount = models.IntegerField(default=0)
- time = models.DurationField()
- plan_id = models.CharField(max_length=16, choices=SUBSCR_PLAN_CHOICES)
- subscription = models.ForeignKey('Subscription', null=True, blank=True, on_delete=models.CASCADE)
- status_message = models.TextField(blank=True, null=True)
- ip_address = models.GenericIPAddressField(blank=True, null=True)
-
- backend_extid = models.CharField(max_length=256, null=True, blank=True)
- backend_data = JSONField(blank=True)
-
- refund_date = models.DateTimeField(blank=True, null=True)
- refund_text = models.CharField(max_length=200, blank=True)
-
- @property
- def currency_code(self):
- return CURRENCY_CODE
-
- @property
- def currency_name(self):
- return CURRENCY_NAME
-
- @property
- def backend(self):
- """ Returns a global instance of the backend
- :rtype: BackendBase
- """
- return BACKENDS[self.backend_id]
-
- def get_amount_display(self):
- return '%.2f %s' % (self.amount / 100, CURRENCY_NAME)
-
- @property
- def is_confirmed(self):
- return self.status == 'confirmed'
-
- def confirm(self):
- self.user.vpnuser.add_paid_time(self.time)
- self.user.vpnuser.on_payment_confirmed(self)
- self.user.vpnuser.save()
- self.update_status('confirmed')
-
- def refund(self):
- self.user.vpnuser.remove_paid_time(self.time)
- self.user.vpnuser.save()
- self.update_status('refunded')
-
- def update_status(self, status, message=None):
- assert any(c[0] == status for c in STATUS_CHOICES)
- self.status = status
- self.status_message = message
-
- class Meta:
- ordering = ('-created', )
-
- @classmethod
- def create_payment(self, backend_id, user, months):
- payment = Payment(
- user=user,
- backend_id=backend_id,
- status='new',
- time=timedelta(days=30 * months),
- amount=get_price() * months
- )
- return payment
-
-
-class Subscription(models.Model, BackendData):
- """ Recurring payment subscription. """
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- backend_id = models.CharField(max_length=16, choices=BACKEND_CHOICES)
- created = models.DateTimeField(auto_now_add=True)
- plan_id = models.CharField(max_length=16, choices=SUBSCR_PLAN_CHOICES)
- next_payment_date = models.DateTimeField(blank=True, null=True)
- last_confirmed_payment = models.DateTimeField(blank=True, null=True)
- status = models.CharField(max_length=16, choices=SUBSCR_STATUS_CHOICES, default='new')
-
- backend_extid = models.CharField(max_length=256, null=True, blank=True)
- backend_data = JSONField(blank=True)
-
- @property
- def backend(self):
- """ Returns a global instance of the backend
- :rtype: BackendBase
- """
- return BACKENDS[self.backend_id]
-
- @property
- def months(self):
- return period_months(self.period)
-
- @property
- def period_amount(self):
- return self.months * get_price()
-
- @property
- def next_renew(self):
- """ Approximate date of the next payment """
- if self.last_confirmed_payment:
- return self.last_confirmed_payment + timedelta(days=self.months * 30)
- return self.created + timedelta(days=self.months * 30)
-
- @property
- def monthly_amount(self):
- return get_price()
-
- def create_payment(self):
- payment = Payment(
- user=self.user,
- backend_id=self.backend_id,
- status='new',
- time=timedelta(days=30 * self.months),
- amount=get_price() * self.months,
- subscription=self,
- )
- return payment
-
-
-class Feedback(models.Model):
- user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
- subscription = models.ForeignKey('Subscription', null=True, blank=True, on_delete=models.SET_NULL)
- created = models.DateTimeField(auto_now_add=True)
- message = models.TextField()
-
diff --git a/payments/tasks.py b/payments/tasks.py
deleted file mode 100644
index 2430f95..0000000
--- a/payments/tasks.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import logging
-from datetime import timedelta
-from django.utils import timezone
-from celery import task
-
-from payments.models import Payment
-from .models import Payment, Subscription, ACTIVE_BACKENDS
-
-logger = logging.getLogger(__name__)
-
-@task
-def check_subscriptions():
- logger.debug("checking subscriptions")
- subs = Subscription.objects.filter(status='active', backend_id='stripe').all()
- for sub in subs:
- logger.debug("checking subscription #%s on %s", sub.id, sub.backend_id)
- sub.refresh_from_db()
- ACTIVE_BACKENDS['stripe'].refresh_subscription(sub)
- sub.save()
-
-@task
-def cancel_old_payments():
- expdate = timezone.now() - timedelta(days=3)
-
- expired = Payment.objects.filter(created__lte=expdate, status='new',
- paid_amount=0)
-
- logger.info("cancelling %d pending payments older than 3 days (%s)", len(expired), expdate.isoformat())
-
- for p in expired:
- logger.debug("cancelling payment #%d (%s): created on %s", p.id, p.user.username, p.created)
- p.status = 'cancelled'
- p.save()
diff --git a/payments/tests/__init__.py b/payments/tests/__init__.py
deleted file mode 100644
index 118c9ab..0000000
--- a/payments/tests/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# flake8: noqa
-
-from .bitcoin import *
-from .paypal import *
-from .coingate import *
-
-from django.conf import settings
-if settings.RUN_ONLINE_TESTS:
- from .online.stripe import *
-
diff --git a/payments/tests/bitcoin.py b/payments/tests/bitcoin.py
deleted file mode 100644
index 44ec080..0000000
--- a/payments/tests/bitcoin.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from datetime import timedelta
-
-from django.test import TestCase
-from django.http import HttpResponseRedirect
-from django.contrib.auth.models import User
-from constance.test import override_config
-
-from payments.models import Payment
-from payments.backends import BitcoinBackend
-
-from decimal import Decimal
-
-
-class FakeBTCRPCNew:
- def getnewaddress(self, account):
- return 'TEST_ADDRESS'
-
-
-class FakeBTCRPCUnpaid:
- def getreceivedbyaddress(self, address):
- assert address == 'TEST_ADDRESS'
- return Decimal('0')
-
-
-class FakeBTCRPCPartial:
- def getreceivedbyaddress(self, address):
- assert address == 'TEST_ADDRESS'
- return Decimal('0.5') * 100000000
-
-
-class FakeBTCRPCPaid:
- def getreceivedbyaddress(self, address):
- assert address == 'TEST_ADDRESS'
- return Decimal('1') * 100000000
-
-
-class BitcoinBackendTest(TestCase):
- def setUp(self):
- self.user = User.objects.create_user('test', 'test_user@example.com', None)
-
- self.p = Payment.objects.create(
- user=self.user, time=timedelta(days=30), backend_id='bitcoin',
- amount=300)
-
- @override_config(BTC_EUR_VALUE=300)
- def test_new(self):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCNew
-
- backend.new_payment(self.p)
- redirect = backend.new_payment(self.p)
- self.assertEqual(self.p.backend_extid, 'TEST_ADDRESS')
- self.assertEqual(self.p.status, 'new')
- self.assertIn('btc_price', self.p.backend_data)
- self.assertIn('btc_address', self.p.backend_data)
- self.assertEqual(self.p.backend_data['btc_address'], 'TEST_ADDRESS')
- self.assertIsInstance(redirect, HttpResponseRedirect)
- self.assertEqual(redirect.url, '/payments/view/%d' % self.p.id)
- self.assertEqual(self.p.status_message, "Please send 1.00000 BTC to TEST_ADDRESS")
-
- def test_rounding(self):
- """ Rounding test
- 300 / 300 = 1 => 1.00000 BTC
- 300 / 260 = Decimal('1.153846153846153846153846154') => 1.15385 BTC
- """
- with override_config(BTC_EUR_VALUE=300):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCNew
- backend.new_payment(self.p)
- self.assertEqual(self.p.status_message, "Please send 1.00000 BTC to TEST_ADDRESS")
-
- with override_config(BTC_EUR_VALUE=260):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCNew
- backend.new_payment(self.p)
- self.assertEqual(self.p.status_message, "Please send 1.15385 BTC to TEST_ADDRESS")
-
-
-class BitcoinBackendConfirmTest(TestCase):
- @override_config(BTC_EUR_VALUE=300)
- def setUp(self):
- self.user = User.objects.create_user('test', 'test_user@example.com', None)
-
- self.p = Payment.objects.create(
- user=self.user, time=timedelta(days=30), backend_id='bitcoin',
- amount=300)
-
- # call new_payment
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCNew
- backend.new_payment(self.p)
-
- @override_config(BTC_EUR_VALUE=300)
- def test_check_unpaid(self):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCUnpaid
-
- backend.check(self.p)
- self.assertEqual(self.p.status, 'new')
- self.assertEqual(self.p.paid_amount, 0)
-
- @override_config(BTC_EUR_VALUE=300)
- def test_check_partially_paid(self):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCPartial
- backend.check(self.p)
- self.assertEqual(self.p.status, 'new')
- self.assertEqual(self.p.paid_amount, 150)
-
- @override_config(BTC_EUR_VALUE=300)
- def test_check_paid(self):
- backend = BitcoinBackend(dict(URL=''))
- backend.make_rpc = FakeBTCRPCPaid
- backend.check(self.p)
- self.assertEqual(self.p.paid_amount, 300)
- self.assertEqual(self.p.status, 'confirmed')
-
-
diff --git a/payments/tests/coingate.py b/payments/tests/coingate.py
deleted file mode 100644
index 3c9150d..0000000
--- a/payments/tests/coingate.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from datetime import timedelta
-
-from django.test import TestCase, RequestFactory
-from django.http import HttpResponseRedirect
-from django.contrib.auth.models import User
-
-from payments.models import Payment
-from payments.backends import CoinGateBackend
-
-
-class CoinGateBackendTest(TestCase):
- def setUp(self):
- self.user = User.objects.create_user('test', 'test_user@example.com', None)
-
- self.backend_settings = dict(
- api_token='test',
- title='Test Title',
- currency='EUR',
- )
-
- def test_payment(self):
- payment = Payment.objects.create(
- user=self.user,
- time=timedelta(days=30),
- backend_id='coingate',
- amount=300
- )
-
- def fake_post(_backend, *, data={}):
- self.assertEqual(data['order_id'], '1')
- self.assertEqual(data['price_amount'], 3.0)
- self.assertEqual(data['price_currency'], 'EUR')
- self.assertEqual(data['receive_currency'], 'EUR')
- return {'id': 42, 'payment_url': 'http://testtoken/'}
-
- with self.settings(ROOT_URL='root'):
- backend = CoinGateBackend(self.backend_settings)
- backend._post = fake_post
- redirect = backend.new_payment(payment)
-
- self.assertIsInstance(redirect, HttpResponseRedirect)
- self.assertEqual(redirect.url, 'http://testtoken/')
- self.assertEqual(payment.backend_data.get('coingate_id'), 42)
-
-
- # Test a standard successful payment callback flow
-
- def post_callback(status):
- callback_data = {
- 'token': payment.backend_data['coingate_token'],
- 'order_id': str(payment.id),
- 'status': status,
- }
- ipn_url = '/payments/callback/coingate/%d' % payment.id
- ipn_request = RequestFactory().post(
- ipn_url,
- data=callback_data)
- return backend.callback(payment, ipn_request)
-
- r = post_callback('pending')
- self.assertTrue(r)
- self.assertEqual(payment.status, 'new')
-
- r = post_callback('confirming')
- self.assertTrue(r)
- self.assertEqual(payment.status, 'new')
-
- r = post_callback('paid')
- self.assertTrue(r)
- self.assertEqual(payment.status, 'confirmed')
- self.assertEqual(payment.paid_amount, 300)
-
- time_left_s = self.user.vpnuser.time_left.total_seconds()
- self.assertAlmostEqual(time_left_s, payment.time.total_seconds(), delta=60)
-
diff --git a/payments/tests/paypal.py b/payments/tests/paypal.py
deleted file mode 100644
index 96dcda4..0000000
--- a/payments/tests/paypal.py
+++ /dev/null
@@ -1,325 +0,0 @@
-from datetime import timedelta
-from urllib.parse import parse_qs
-
-from django.test import TestCase, RequestFactory
-from django.http import HttpResponseRedirect
-from django.contrib.auth.models import User
-
-from payments.models import Payment, Subscription
-from payments.backends import PaypalBackend
-
-
-PAYPAL_IPN_TEST = '''\
-mc_gross=3.00&\
-protection_eligibility=Eligible&\
-address_status=confirmed&\
-payer_id=LPLWNMTBWMFAY&\
-tax=0.00&\
-address_street=1+Main+St&\
-payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&\
-payment_status=Completed&\
-charset=windows-1252&\
-address_zip=95131&\
-first_name=Test&\
-mc_fee=0.88&\
-address_country_code=US&\
-address_name=Test+User&\
-notify_version=2.6&\
-custom=&\
-payer_status=verified&\
-address_country=United+States&\
-address_city=San+Jose&\
-quantity=1&\
-payer_email=test_user@example.com&\
-txn_id=61E67681CH3238416&\
-payment_type=instant&\
-last_name=User&\
-address_state=CA&\
-receiver_email=test_business@example.com&\
-payment_fee=0.88&\
-receiver_id=S8XGHLYDW9T3S&\
-txn_type=express_checkout&\
-item_name=&\
-mc_currency=EUR&\
-item_number=&\
-residence_country=US&\
-test_ipn=1&\
-handling_amount=0.00&\
-transaction_subject=&\
-payment_gross=3.00&\
-shipping=0.00'''
-
-PAYPAL_IPN_SUBSCR_PAYMENT = '''\
-transaction_subject=VPN+Payment&\
-payment_date=11%3A19%3A00+Sep+04%2C+2016+PDT&\
-txn_type=subscr_payment&\
-subscr_id=I-1S262863X133&\
-last_name=buyer&\
-residence_country=FR&\
-item_name=VPN+Payment&\
-payment_gross=&\
-mc_currency=EUR&\
-business=test_business@example.com&\
-payment_type=instant&\
-protection_eligibility=Ineligible&\
-payer_status=verified&\
-test_ipn=1&\
-payer_email=test_user@example.com&\
-txn_id=097872679P963871Y&\
-receiver_email=test_business@example.com&\
-first_name=test&\
-payer_id=APYYVSFLNPWUU&\
-receiver_id=MGT8TQ8GC4944&\
-payment_status=Completed&\
-payment_fee=&\
-mc_fee=0.56&\
-mc_gross=9.00&\
-charset=windows-1252&\
-notify_version=3.8&\
-ipn_track_id=546a4aa4300a0'''
-
-
-PAYPAL_IPN_SUBSCR_CANCEL = '''\
-txn_type=subscr_cancel&\
-subscr_id=I-E5SCT6936H40&\
-last_name=buyer&\
-residence_country=FR&\
-mc_currency=EUR&\
-item_name=VPN+Payment&\
-business=test_business@example.com&\
-recurring=1&\
-payer_status=verified&\
-test_ipn=1&\
-payer_email=test_user@example.com&\
-first_name=test&\
-receiver_email=test_business@example.com&\
-payer_id=APYYVSFLNPWUU&\
-reattempt=1&\
-subscr_date=17%3A35%3A14+Sep+04%2C+2016+PDT&\
-charset=windows-1252&\
-notify_version=3.8&\
-period3=3+M&\
-mc_amount3=9.00&\
-ipn_track_id=474870d13b375'''
-
-
-PAYPAL_IPN_SUBSCR_SIGNUP = '''\
-txn_type=subscr_signup&\
-subscr_id=I-1S262863X133&\
-last_name=buyer&\
-residence_country=FR&\
-mc_currency=EUR&\
-item_name=VPN+Payment&\
-business=test_business@example.com&\
-recurring=1&\
-payer_status=verified&\
-test_ipn=1&\
-payer_email=test_user@example.com&\
-first_name=test&\
-receiver_email=test_business@example.com&\
-payer_id=APYYVSFLNPWUU&\
-reattempt=1&\
-subscr_date=11%3A18%3A57+Sep+04%2C+2016+PDT&\
-charset=windows-1252&\
-notify_version=3.8&\
-period3=3+M&\
-mc_amount3=9.00&\
-ipn_track_id=546a4aa4300a0'''
-
-
-class PaypalBackendTest(TestCase):
- def setUp(self):
- self.user = User.objects.create_user('test', 'test_user@example.com', None)
-
- def test_paypal(self):
- # TODO: This checks the most simple and perfect payment that could
- # happen, but not errors or other/invalid IPN
-
- payment = Payment.objects.create(
- user=self.user,
- time=timedelta(days=30),
- backend_id='paypal',
- amount=300
- )
-
- settings = dict(
- test=True,
- title='Test Title',
- currency='EUR',
- address='test_business@example.com',
- )
-
- with self.settings(ROOT_URL='root'):
- backend = PaypalBackend(settings)
- redirect = backend.new_payment(payment)
-
- self.assertIsInstance(redirect, HttpResponseRedirect)
-
- host, params = redirect.url.split('?', 1)
- params = parse_qs(params)
-
- expected_notify_url = 'root/payments/callback/paypal/%d' % payment.id
- expected_return_url = 'root/payments/view/%d' % payment.id
- expected_cancel_url = 'root/payments/cancel/%d' % payment.id
-
- self.assertEqual(params['cmd'][0], '_xclick')
- self.assertEqual(params['notify_url'][0], expected_notify_url)
- self.assertEqual(params['return'][0], expected_return_url)
- self.assertEqual(params['cancel_return'][0], expected_cancel_url)
- self.assertEqual(params['business'][0], 'test_business@example.com')
- self.assertEqual(params['currency_code'][0], 'EUR')
- self.assertEqual(params['amount'][0], '3.00')
- self.assertEqual(params['item_name'][0], 'Test Title')
-
- # Replace PaypalBackend.verify_ipn to not call the PayPal API
- # we will assume the IPN is authentic
- backend.verify_ipn = lambda request: True
-
- ipn_url = '/payments/callback/paypal/%d' % payment.id
- ipn_request = RequestFactory().post(
- ipn_url,
- content_type='application/x-www-form-urlencoded',
- data=PAYPAL_IPN_TEST)
- r = backend.callback(payment, ipn_request)
-
- self.assertTrue(r)
- self.assertEqual(payment.status, 'confirmed')
- self.assertEqual(payment.paid_amount, 300)
- self.assertEqual(payment.backend_extid, '61E67681CH3238416')
-
- def test_paypal_ipn_error(self):
- payment = Payment.objects.create(
- user=self.user,
- time=timedelta(days=30),
- backend_id='paypal',
- amount=300
- )
-
- settings = dict(
- test=True,
- title='Test Title',
- currency='EUR',
- address='test_business@example.com',
- )
-
- with self.settings(ROOT_URL='root'):
- backend = PaypalBackend(settings)
- redirect = backend.new_payment(payment)
-
- self.assertIsInstance(redirect, HttpResponseRedirect)
-
- host, params = redirect.url.split('?', 1)
- params = parse_qs(params)
-
- # Replace PaypalBackend.verify_ipn to not call the PayPal API
- # we will assume the IPN is authentic
- backend.verify_ipn = lambda request: True
-
- ipn_url = '/payments/callback/paypal/%d' % payment.id
- ipn_request = RequestFactory().post(
- ipn_url,
- content_type='application/x-www-form-urlencoded',
- data=PAYPAL_IPN_TEST)
- r = backend.callback(payment, ipn_request)
-
- self.assertTrue(r)
- self.assertEqual(payment.status, 'confirmed')
- self.assertEqual(payment.paid_amount, 300)
- self.assertEqual(payment.backend_extid, '61E67681CH3238416')
-
- def test_paypal_subscr(self):
- subscription = Subscription.objects.create(
- user=self.user,
- backend_id='paypal',
- period='3m'
- )
-
- settings = dict(
- test=True,
- title='Test Title',
- currency='EUR',
- address='test_business@example.com',
- )
-
- with self.settings(ROOT_URL='root'):
- backend = PaypalBackend(settings)
- redirect = backend.new_subscription(subscription)
-
- self.assertIsInstance(redirect, HttpResponseRedirect)
-
- host, params = redirect.url.split('?', 1)
- params = parse_qs(params)
-
- expected_notify_url = 'root/payments/callback/paypal_subscr/%d' % subscription.id
- expected_return_url = 'root/payments/return_subscr/%d' % subscription.id
- expected_cancel_url = 'root/account/'
-
- self.assertEqual(params['cmd'][0], '_xclick-subscriptions')
- self.assertEqual(params['notify_url'][0], expected_notify_url)
- self.assertEqual(params['return'][0], expected_return_url)
- self.assertEqual(params['cancel_return'][0], expected_cancel_url)
- self.assertEqual(params['business'][0], 'test_business@example.com')
- self.assertEqual(params['currency_code'][0], 'EUR')
- self.assertEqual(params['a3'][0], '9.00')
- self.assertEqual(params['p3'][0], '3')
- self.assertEqual(params['t3'][0], 'M')
- self.assertEqual(params['item_name'][0], 'Test Title')
-
- # Replace PaypalBackend.verify_ipn to not call the PayPal API
- # we will assume the IPN is authentic
- backend.verify_ipn = lambda request: True
-
- self.assertEqual(subscription.status, 'new')
-
- # 1. the subscr_payment IPN
- ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id
- ipn_request = RequestFactory().post(
- ipn_url,
- content_type='application/x-www-form-urlencoded',
- data=PAYPAL_IPN_SUBSCR_PAYMENT)
- r = backend.callback_subscr(subscription, ipn_request)
-
- self.assertTrue(r)
- self.assertEqual(subscription.status, 'active')
- self.assertEqual(subscription.backend_extid, 'I-1S262863X133')
-
- payments = Payment.objects.filter(subscription=subscription).all()
- self.assertEqual(len(payments), 1)
- self.assertEqual(payments[0].amount, 900)
- self.assertEqual(payments[0].paid_amount, 900)
- self.assertEqual(payments[0].backend_extid, '097872679P963871Y')
-
- # 2. the subscr_signup IPN
- # We don't expect anything to happen here
- ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id
- ipn_request = RequestFactory().post(
- ipn_url,
- content_type='application/x-www-form-urlencoded',
- data=PAYPAL_IPN_SUBSCR_SIGNUP)
- r = backend.callback_subscr(subscription, ipn_request)
-
- self.assertTrue(r)
- self.assertEqual(subscription.status, 'active')
- self.assertEqual(subscription.backend_extid, 'I-1S262863X133')
-
- payments = Payment.objects.filter(subscription=subscription).all()
- self.assertEqual(len(payments), 1)
- self.assertEqual(payments[0].amount, 900)
- self.assertEqual(payments[0].paid_amount, 900)
- self.assertEqual(payments[0].backend_extid, '097872679P963871Y')
-
- # 3. the subscr_cancel IPN
- ipn_url = '/payments/callback/paypal_subscr/%d' % subscription.id
- ipn_request = RequestFactory().post(
- ipn_url,
- content_type='application/x-www-form-urlencoded',
- data=PAYPAL_IPN_SUBSCR_CANCEL)
- r = backend.callback_subscr(subscription, ipn_request)
-
- self.assertTrue(r)
- self.assertEqual(subscription.status, 'cancelled')
- self.assertEqual(subscription.backend_extid, 'I-1S262863X133')
-
- payments = Payment.objects.filter(subscription=subscription).all()
- self.assertEqual(len(payments), 1)
-
diff --git a/payments/urls.py b/payments/urls.py
deleted file mode 100644
index 2567584..0000000
--- a/payments/urls.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from django.conf.urls import url
-from . import views
-
-app_name = 'payments'
-
-urlpatterns = [
- url(r'^new$', views.new),
- url(r'^view/(?P