Commit 31c58171 authored by Birin Sanchez's avatar Birin Sanchez
Browse files

Refactoring: Add new Subscription class used to wrap all the WC


subscription related operations.

Changes:

* Clean up several modules moving subscription functionality to the
  new Subscription class.

* Modify all views and commands so the make use of the new
  Subscription class.

* Remove ExternalCredit model.
Signed-off-by: Birin Sanchez's avatarBirin Sanchez <birin.sanchez@puri.sm>
parent 4c572f21
Pipeline #35889 passed with stage
in 32 seconds
......@@ -4,10 +4,12 @@ from django.views.generic.edit import FormView
from django.contrib.auth.mixins import LoginRequiredMixin
from .forms import ChooseBundleForm
from .models import BundleChange
from limitmonitor.common import parse_subscription, set_subscription_status, \
SubscriptionParseError
from limitmonitor.common import active_subscription, can_change_bundle, \
invitees_clean
from limitmonitor.models import get_available_bundles
from limitmonitor.task_resources.subscription import create_user_subscription
from limitmonitor.subscription import BillingPeriod, SubscriptionStatus, \
SubscriptionManager
from purist.models import AccountType
class UpgradeView(LoginRequiredMixin, FormView):
......@@ -19,7 +21,12 @@ class UpgradeView(LoginRequiredMixin, FormView):
def get_form_kwargs(self):
self.avl_bundles = get_available_bundles()
choices = self.request.user.get_upgrade_choices(add_downgrade=True)
self.sub = active_subscription(self.request.user)
if self.sub:
choices = self.sub.upgrade_choices(add_downgrade=True)
else:
choices = []
kwargs = super(UpgradeView, self).get_form_kwargs()
kwargs['choices'] = choices
return kwargs
......@@ -36,10 +43,11 @@ class UpgradeView(LoginRequiredMixin, FormView):
pending_changes = self.request.user.bundlechange_set.filter(
is_pending=True
)
u_ec = self.request.user.get_external_credit()
if u_ec is not None:
# sub = active_subscription(self.request.user)
if self.sub is not None:
context['current_bdl'] = (
self.avl_bundles[u_ec.bundle_key]['short_title'])
self.avl_bundles[str(self.sub.variation_id)]['short_title'])
if len(pending_changes) > 0:
context['has_pending_changes'] = True
......@@ -50,44 +58,38 @@ class UpgradeView(LoginRequiredMixin, FormView):
context['pay_url'] = pending_changes[0].pay_url
else:
context['has_pending_changes'] = False
context['can_upgrade'] = self.request.user.can_change_bundle()
context['invitees_clean'] = self.request.user.invitees_clean()
context['can_upgrade'] = can_change_bundle(self.request.user)
context['invitees_clean'] = invitees_clean(self.request.user)
return context
def form_valid(self, form):
variation_id = form.cleaned_data['bundle']
subs = self.request.user.get_woocommerce_subscriptions()
# We assume the user has only 1 active subscription!
if subs:
try:
parsed_sub = parse_subscription(subs[0])
except SubscriptionParseError as e:
print('Error processing bundle_change for user {}: {}'.format(
self.request.user, repr(e)))
return super(UpgradeView, self).form_valid(form)
acc_type = self.avl_bundles[variation_id]['type']
new_period = self.avl_bundles[variation_id]['period']
order = create_user_subscription(self.request.user,
bundle_type=acc_type,
billing_period=new_period,
status='pending')
pay_url = settings.WOO_UPGRADE_PAY_URL.format(order['id'],
order['key'])
acc_type = AccountType.from_str(
self.avl_bundles[variation_id]['type']
)
period = BillingPeriod(self.avl_bundles[variation_id]['period'])
if self.sub:
new_sub = SubscriptionManager.create_subscription(
self.request.user, account_type=acc_type,
billing_period=period
)
pay_url = settings.WOO_UPGRADE_PAY_URL.format(new_sub.order_id,
new_sub.order_key)
bundle_change = BundleChange(
user=self.request.user,
from_bdl=parsed_sub['bundle_key'],
from_bdl=self.sub.variation_id,
to_bdl=variation_id,
is_pending=True,
pay_url=pay_url,
new_order_id=order['id'],
old_subscription_id=parsed_sub['external_key']
new_order_id=new_sub.order_id,
old_subscription_id=self.sub.subscription_id
)
bundle_change.save()
set_subscription_status(parsed_sub['external_key'],
'pending-cancel')
# Deactivate previous subscription
self.sub.status = SubscriptionStatus.PENDING_CANCEL
self.sub.save()
return super(UpgradeView, self).form_valid(form)
......@@ -8,7 +8,7 @@ from cart.models import ChosenReward
from purist.models import AccountType
from .forms import CartRegistrationFormWithCaptcha, CartRegistrationForm
from limitmonitor.common import forced_update
from limitmonitor.task_resources.subscription import create_user_subscription
from limitmonitor.subscription import SubscriptionManager, SubscriptionStatus
import json
......@@ -77,7 +77,11 @@ class CartRegistrationView(RegistrationView):
if self.bundle == 'Basic':
user.account_type = AccountType.BASIC
create_user_subscription(user, paid=True, billing_email=user.email)
SubscriptionManager.create_subscription(
user, paid=True,
status=SubscriptionStatus.ACTIVE,
billing_email=user.email
)
forced_update(user)
elif self.bundle == 'Complete':
......
......@@ -8,8 +8,7 @@ from registration.backends.simple.views import RegistrationView
from purist.models import AccountType
from cart.views import CartRegistrationFormWithCaptcha as \
InvitationRegistrationForm
from limitmonitor.task_resources.subscription import \
create_invitee_subscription
from limitmonitor.subscription import SubscriptionManager
from limitmonitor.common import forced_update
......@@ -67,7 +66,7 @@ class InvitationRegistrationView(RegistrationView):
self.invitation.save()
# Create an Invitee subscritpion for the user
sub = create_invitee_subscription(user)
sub = SubscriptionManager.create_invitee_subscription(user)
if sub:
forced_update(user)
......
from django.contrib import admin
from .models import ExternalBundle, ExternalCredit, Limit
from .models import ExternalBundle, Limit
#
......@@ -12,10 +12,6 @@ class ExternalBundleAdmin(admin.ModelAdmin):
list_display = ['id', 'parser', 'external_key', 'service', 'time_credit', 'volume_credit']
class ExternalCreditAdmin(admin.ModelAdmin):
list_display = ['label', 'external_code', 'bundle_key', 'account_name', 'is_converted', 'error_message']
class LimitAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'service', 'is_active', 'renewal_date', 'expiry_date', 'volume_total', 'time_total']
list_filter = ['service', 'renewal_date', 'user']
......@@ -25,5 +21,4 @@ class LimitAdmin(admin.ModelAdmin):
#
admin.site.register(ExternalBundle, ExternalBundleAdmin)
admin.site.register(ExternalCredit, ExternalCreditAdmin)
admin.site.register(Limit, LimitAdmin)
import logging
import datetime
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from purist.models import AccountType
from middleware.common import get_woo_connection
from purist.limitmonitor import ParserContainer, ServicesContainer
from limitmonitor.models import ExternalCredit, ExternalBundle, Limit, \
get_available_bundles
from purist.limitmonitor import ServicesContainer
from limitmonitor.models import ExternalBundle, Limit
from limitmonitor.tunnel import TunnelManager
from limitmonitor.subscription import SubscriptionManager, SubscriptionStatus
logger = logging.getLogger(__name__)
class SubscriptionParseError(Exception):
"""Raised when the subscription being parsed contains not expected
data.
"""
pass
class OrderParseError(Exception):
"""Raised when the order being parsed contains not expected data.
......@@ -42,123 +33,6 @@ def get_username_from_woo_customer_id(customer_id, woo=None):
return "invalid"
def parse_subscription(json_entry):
subscription_id = str(json_entry["id"])
# validation
if len(json_entry["line_items"]) != 1:
raise SubscriptionParseError(
"Subscription {} does not have one line_items".format(
subscription_id)
)
line_item = json_entry["line_items"][0]
quantity = line_item["quantity"]
if quantity != 1:
raise SubscriptionParseError(
"Bad quantity {} in subscription {}".format(
quantity, subscription_id)
)
# calculate next renewal date
if json_entry["next_payment_date"] != "":
next_renewal_naive = datetime.datetime.strptime(
json_entry["next_payment_date"], "%Y-%m-%dT%H:%M:%S")
next_renewal = timezone.make_aware(next_renewal_naive)
else:
next_renewal = datetime.datetime(1900, 1, 1, 0, 0)
# get account name
username = get_username_from_woo_customer_id(json_entry["customer_id"])
# bundle_key value is different depending on the subscription
# having variations or not
if line_item.get('variation_id') != 0:
bundle_key = line_item.get('variation_id')
else:
bundle_key = line_item.get('product_id')
# get account_type and billing_period from configured bundles
account_type = AccountType.UNDEFINED
billing_period = None
bundles = get_available_bundles()
sub_bundle = bundles.get(str(bundle_key), None)
if sub_bundle:
sub_acc_type = sub_bundle.get('type', None)
if 'GROUP' == sub_acc_type:
account_type = AccountType.GROUP
elif 'COMPLETE' == sub_acc_type:
account_type = AccountType.COMPLETE
elif 'BASIC' == sub_acc_type:
account_type = AccountType.BASIC
elif 'INVITED' == sub_acc_type:
account_type = AccountType.INVITED
sub_period = sub_bundle.get('period', None)
if 'monthly' == sub_period:
billing_period = 'monthly'
elif 'annual' == sub_period:
billing_period = 'annual'
# create result
result = {
"parser": ParserContainer.WOO_SUBSCRIPTION_V1,
"external_key": subscription_id,
"label": json_entry["number"],
"bundle_key": bundle_key,
"bundle_label": str(line_item["name"]),
"quantity": 1,
"account": username,
"next_renewal": next_renewal,
"status": json_entry["status"],
"account_type": account_type,
"billing_period": billing_period
}
return result
def subscription_was_processed(parsed_sub, overwrite=False):
"""Returns an ExternalCredit from the DB if the subscription was
already processed. If the subscription was not already processed
it returns a new ExternalCredit created from parsed_sub data with
is_converted=False.
If overwrite is set to True the existing ExternalCredit is
overwritten with new values from parsed_sub.
"""
try:
ext_cred = ExternalCredit.objects.get(
external_key=parsed_sub['external_key'])
if overwrite:
ext_cred.parser = parsed_sub["parser"]
ext_cred.external_key = parsed_sub["external_key"]
ext_cred.label = parsed_sub["label"]
ext_cred.bundle_key = parsed_sub["bundle_key"]
ext_cred.bundle_label = parsed_sub["bundle_label"]
ext_cred.quantity = parsed_sub["quantity"]
ext_cred.account_name = parsed_sub["account"]
ext_cred.additional_data = "None"
ext_cred.is_converted = False
except ExternalCredit.DoesNotExist:
ext_cred = ExternalCredit(
parser=parsed_sub["parser"],
external_key=parsed_sub["external_key"],
label=parsed_sub["label"],
bundle_key=parsed_sub["bundle_key"],
bundle_label=parsed_sub["bundle_label"],
quantity=parsed_sub["quantity"],
account_name=parsed_sub["account"],
additional_data="None",
is_converted=False,
)
return ext_cred
def get_services_from_bundle(bundle_key):
ext_buns = ExternalBundle.objects.filter(external_key=bundle_key)
result = []
......@@ -182,68 +56,47 @@ def update_services(user, srv_to_disable, srv_to_enable, renewal_date):
def forced_update(user):
subscriptions = user.get_woocommerce_subscriptions()
if subscriptions is None:
subscriptions = SubscriptionManager.search_subscriptions(user)
if len(subscriptions) == 0:
logger.info('User {} has no subscriptions.'.format(user.username))
return
for sub in subscriptions:
try:
parsed_sub = parse_subscription(sub)
except SubscriptionParseError as parse_e:
logger.info(repr(parse_e))
continue
if parsed_sub['bundle_key'] not in settings.WOOSUB1_PRODUCT_LIST:
if sub.variation_id not in settings.WOOSUB1_PRODUCT_LIST:
logger.info('Subscription {} for user {} does not belong '
'to {}'.format(parsed_sub['bundle_key'],
'to {}'.format(sub.subscription_id,
user.username,
settings.SITE_TITLE))
continue # Skip this subscription as is not a product we care of
ext_cred = subscription_was_processed(parsed_sub, overwrite=True)
error = 'Subscription successfully enabled through forced_update'
ext_cred.updated_date = timezone.now()
bndl_srvs = set(get_services_from_bundle(parsed_sub['bundle_key']))
bndl_srvs = set(get_services_from_bundle(sub.variation_id))
cur_srvs = set(user.get_enabled_limits())
if parsed_sub['status'] == 'active':
if sub.status == SubscriptionStatus.ACTIVE:
srv_to_enable = bndl_srvs - cur_srvs
srv_to_disable = cur_srvs - bndl_srvs
srv_to_reenable = bndl_srvs & cur_srvs
is_pending = False
is_converted = True
try:
update_services(user, srv_to_disable,
srv_to_enable | srv_to_reenable,
parsed_sub['next_renewal'])
except TunnelManager.TerminateAccountError as e1:
error = '{}: {}'.format(ext_cred.external_key, repr(e1))
is_pending = True
is_converted = False
except Exception as e2:
is_pending = True
is_converted = False
error = '{}: {}'.format(ext_cred.external_key, repr(e2))
finally:
sub.next_payment_date)
user.account_type = sub.account_type
user.save()
if hasattr(user, 'chosenreward'):
user.chosenreward.is_pending = is_pending
user.chosenreward.is_pending = False
user.chosenreward.save()
ext_cred.error_message = error
ext_cred.is_converted = is_converted
ext_cred.save()
if is_converted:
# Set account_type as specified by subscription
# when conversion succeeds.
user.account_type = parsed_sub['account_type']
user.save()
except TunnelManager.TerminateAccountError as e1:
error = '{}: {}'.format(sub.subscription_id, repr(e1))
logger.error(error)
except Exception as e2:
error = '{}: {}'.format(sub.subscription_id, repr(e2))
logger.error(error)
# TODO: process other subscription statuses
else:
logger.info('Subscription {} for user {} status is: {}'.format(
parsed_sub['label'], user.username, parsed_sub['status']))
sub.subscription_id, user.username, sub.status.value))
def parse_order(order):
......@@ -306,12 +159,10 @@ def delete_account(user, purge_n=0, purge=False):
user.save()
# Cancell all user subscriptions
subs = user.get_woocommerce_subscriptions()
if subs:
for sub in subs:
sub_id = sub.get('id', None)
if sub_id:
set_subscription_status(sub_id, 'cancelled')
subs = SubscriptionManager.search_subscriptions(user)
for sub in subs:
sub.status = SubscriptionStatus.CANCELLED
sub.save()
# Remove WC PII
user.remove_woocommerce_pii()
......@@ -368,40 +219,51 @@ def order_paid(order_id):
return False
def set_subscription_status(subscription_id, status):
"""Given a subscription_id and a status it set that status for the
subscription returning True when it succeeds and False when it
does not.
def can_change_bundle(user):
if user.account_type == AccountType.INVITED:
return False
else:
sub = active_subscription(user)
if sub:
choices = sub.upgrade_choices(add_downgrade=True)
else:
return False
return len(choices) > 0
This has only be tested with WC instances with Stripe payment
gateway enabled. When used against a WC instance with Check
payments and not Stripe gateway configured it returns the error:
'woocommerce_rest_invalid_payment_data' but the subscription still
get cancelled.
def active_subscription(user):
"""Returns currently active subscription for the user or None if the
user does not have an active subscription.
"""
valid_statuses = ['pending', 'active', 'on-hold', 'expired', 'cancelled',
'pending-cancel']
if status not in valid_statuses:
return False
woo = get_woo_connection()
data = {
'status': status
}
query = 'subscriptions/{}'.format(subscription_id)
result = woo.put(query, data).json()
if result:
r_status = result.get('status', None)
code = result.get('code', None)
if r_status == status:
return True
elif code == 'woocommerce_rest_invalid_payment_data':
# No Stripe gateway error. Let's check if the subscription
# was really cancelled.
sub = woo.get('subscriptions/{}'.format(subscription_id)).json()
if sub:
r_status2 = sub.get('status', None)
if r_status2 == status:
return True
return False
subs = SubscriptionManager.search_subscriptions(
user=user, status=SubscriptionStatus.ACTIVE
)
if len(subs) >= 1:
return subs[0]
else:
return None
def invitees_clean(user):
"""For GROUP accounts returns True if all the invitees have set a
recovery email and have one active Invitee subscription. Return
False otherwise. For other type of accounts it always return True.
"""
if user.account_type != AccountType.GROUP:
return True
else:
for invitee in user.get_invitees():
# Check if the invitee has an active Invitee subscription
sub = active_subscription(invitee)
if sub is None:
# The invitee does not have an active account
return False
if (
sub.account_type != AccountType.INVITED
or invitee.email == ''
):
return False
return True
......@@ -162,7 +162,7 @@ SPDX-License-Identifier: AGPL-3.0
<li><a href="{% url 'password_change' %}">{% trans "Change password" %}</a></li>
<li><a href="{% url 'profile_configure' %}">{% trans "Profile settings" %}</a></li>
<li><a href="{% url 'delete_account' %}">{% trans "Delete Account" %}</a></li>
{% if can_upgrade %}
{% if can_upgrade or pending_upgrade %}
<li><a href="{% url 'upgrade' %}">{% trans "Manage Subscription" %}</a></li>
{% endif %}
</ul>
......
......@@ -3,7 +3,8 @@ from django.core.validators import EmailValidator, ValidationError
from django.conf import settings
from django.utils import timezone
from purist.models import User
from limitmonitor.common import parse_subscription, forced_update
from limitmonitor.common import forced_update, active_subscription
from limitmonitor.subscription import SubscriptionError
class Command(BaseCommand):
......@@ -45,41 +46,32 @@ class Command(BaseCommand):
self.stdout.write(msg)
return
subs = user.get_woocommerce_subscriptions()
sub = active_subscription(user)
if subs is None:
msg = 'User {} does not have any subscriptions.'.format(email)
self.stdout.write(msg)
return
for sub in subs:
parsed_sub = parse_subscription(sub)
if parsed_sub['status'] == 'active':
break
else:
if sub is None:
msg = 'User {} has *no* active subscriptions.'.format(email)
self.stdout.write(msg)
return
new_ren_date = (parsed_sub['next_renewal']
+ timezone.timedelta(days=extra_days))
data = {
'next_payment_date': new_ren_date.strftime('%Y-%m-%d %H:%M:%S')
}
result = user.woo_post_json(
'subscriptions/' + parsed_sub['external_key'], data
)
if result is None:
self.stdout.write('Error: Could not update WC subscription.')
old_ren_date = sub.next_payment_date
sub.next_payment_date += timezone.timedelta(days=extra_days)
try:
sub.save()
except SubscriptionError as e:
self.stdout.write(
'Error: Could not update WC subscription.\n'+repr(e)
)
return
forced_update(user)
msg = (
'\nSubscription for user {} has been extended {} days, from {} to '
'{}.'.format(
email,
extra_days,
parsed_sub['next_renewal'].strftime('%Y-%m-%d'),
new_ren_date.strftime('%Y-%m-%d')
old_ren_date.strftime('%Y-%m-%d'),
sub.next_payment_date.strftime('%Y-%m-%d')
)
)
self.stdout.write(msg)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-09-27 09:22
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('limitmonitor', '0007_add_new_services'),
]
operations = [
</