diff --git a/ccvpn/settings.py b/ccvpn/settings.py index 07c81fc..a6c939f 100644 --- a/ccvpn/settings.py +++ b/ccvpn/settings.py @@ -282,6 +282,14 @@ PAYMENTS_BACKENDS = { PAYMENTS_CURRENCY = ('eur', '€') +PLANS = { + '1m': {'monthly': 3.00, 'months': 1, }, + '3m': {'monthly': 9.00, 'months': 3, }, + '6m': {'monthly': 18.00, 'months': 6, }, + '12m': {'monthly': 36.00, 'months': 12, 'default': 1}, +} + + CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' CONSTANCE_CONFIG = { 'MOTD': ('', "Public site message, displayed on homepage"), diff --git a/payments/admin.py b/payments/admin.py index 649ec2a..c0ff01e 100644 --- a/payments/admin.py +++ b/payments/admin.py @@ -85,14 +85,14 @@ class SubscriptionAdmin(admin.ModelAdmin): model = Subscription list_display = ('user', 'created', 'status', 'backend', 'backend_extid') list_filter = ('backend_id', 'status') - readonly_fields = ('user_link', 'backend', 'period', 'created', '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', 'period', 'payments_links', 'status', + 'fields': ('backend', 'user_link', 'payments_links', 'status', 'last_confirmed_payment'), }), (_("Payment Data"), { diff --git a/payments/migrations/0008_auto_20210721_1931.py b/payments/migrations/0008_auto_20210721_1931.py new file mode 100644 index 0000000..c00c579 --- /dev/null +++ b/payments/migrations/0008_auto_20210721_1931.py @@ -0,0 +1,99 @@ +# 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/models.py b/payments/models.py index c5a53f7..5606389 100644 --- a/payments/models.py +++ b/payments/models.py @@ -89,6 +89,27 @@ 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, @@ -128,12 +149,17 @@ class Payment(models.Model, BackendData): 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 @@ -192,7 +218,8 @@ class Subscription(models.Model, BackendData): 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) + 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')