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

246 lines
7.6 KiB
Python

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))
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()
subscription = models.ForeignKey('Subscription', null=True, blank=True, on_delete=models.CASCADE)
status_message = models.TextField(blank=True, null=True)
backend_extid = models.CharField(max_length=256, null=True, blank=True)
backend_data = JSONField(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)
period = models.CharField(max_length=16, choices=SUBSCR_PERIOD_CHOICES)
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()