Commit 90d94fda authored by Birin Sanchez's avatar Birin Sanchez
Browse files

* Properly deal with Family pack downgrades by downgrading invitees to


  Basic bundle.

* Refuse to downgrade Family pack if invitees don't have a recovery
  email set.

* Create Invitee subscription for new invitees when they set their
  recovery email.

* Move user related methods from limitmonitor.common to purist.models
  module.

* Add methods to create orders and subscriptions.

* process_renewals() now also deals with invitees.
Signed-off-by: Birin Sanchez's avatarBirin Sanchez <birin.sanchez@puri.sm>
parent 1db7e712
Pipeline #21802 passed with stage
in 31 seconds
from django.db import models
from django.conf import settings
from django.utils import timezone
from limitmonitor.models import get_available_bundles
class BundleChange(models.Model):
......@@ -12,3 +13,22 @@ class BundleChange(models.Model):
new_order_id = models.CharField(max_length=100)
old_subscription_id = models.CharField(max_length=100)
created_date = models.DateTimeField(default=timezone.now)
def is_family_downgrade(self):
"""Returns true if this BundleChange instance is a downgrade from a
group bundle (Family pack) to a single bundle (Basic or
Complete). Returns False otherwise."""
bundles = get_available_bundles()
from_bdl = bundles.get(self.from_bdl, None)
to_bdl = bundles.get(self.to_bdl, None)
if from_bdl and to_bdl:
if (
from_bdl.get('type', None) == 'GROUP'
and (
to_bdl.get('type', None) == 'COMPLETE'
or to_bdl.get('type', None) == 'BASIC'
)
):
return True
return False
......@@ -16,16 +16,20 @@
Use <a href="{{ pay_url }}">this link</a> to pay.</p>
<p>{% trans 'After payment is done your account will be updated in few minutes.' %}
{% elif can_upgrade %}
<form id="change_bundle" action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm" />
<a href="{% url 'profile' %}" class="button">No thanks</a>
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% if not invitees_clean %}
<p> {% trans 'You cannot downgrade while one or more invitees are missing a recovery address. Please prompt them to update their details or contact support.' %}</p>
{% else %}
<form id="change_bundle" action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Confirm" />
<a href="{% url 'profile' %}" class="button">No thanks</a>
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endif %}
{% else %}
{% trans 'There is no upgrade or downgrade options for your account.' %}
{% endif %}
......
......@@ -4,9 +4,9 @@ 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 get_user_subscriptions, parse_subscription, \
set_subscription_status, get_user_external_credit, get_available_bundles, \
get_user_upgrade_choices, SubscriptionParseError
from limitmonitor.common import parse_subscription, set_subscription_status, \
SubscriptionParseError
from limitmonitor.models import get_available_bundles
from limitmonitor.task_resources.subscription import upgrade_user_subscription
......@@ -18,8 +18,7 @@ class UpgradeView(LoginRequiredMixin, FormView):
def get_form_kwargs(self):
choices = get_user_upgrade_choices(self.request.user,
add_downgrade=True)
choices = self.request.user.get_upgrade_choices(add_downgrade=True)
kwargs = super(UpgradeView, self).get_form_kwargs()
kwargs['choices'] = choices
return kwargs
......@@ -37,7 +36,7 @@ class UpgradeView(LoginRequiredMixin, FormView):
is_pending=True
)
avl_bundles = get_available_bundles()
u_ec = get_user_external_credit(self.request.user)
u_ec = self.request.user.get_external_credit()
if u_ec is not None:
context['current_bdl'] = (
avl_bundles[u_ec.bundle_key]['short_title'])
......@@ -51,15 +50,14 @@ class UpgradeView(LoginRequiredMixin, FormView):
context['pay_url'] = pending_changes[0].pay_url
else:
context['has_pending_changes'] = False
context['can_upgrade'] = len(get_user_upgrade_choices(
self.request.user,
add_downgrade=True)) > 0
context['can_upgrade'] = self.request.user.can_change_bundle()
context['invitees_clean'] = self.request.user.invitees_clean()
return context
def form_valid(self, form):
variation_id = form.cleaned_data['bundle']
subs = get_user_subscriptions(self.request.user)
subs = self.request.user.get_woocommerce_subscriptions()
# We assume the user has only 1 active subscription!
if subs:
try:
......
......@@ -2,6 +2,7 @@
{% load i18n %}
{% block title %}{% trans 'Invitation' %}{% endblock %}
{% block extra_css%}{% include "purist/spinner.html" %}{% endblock %}
{% block header %}{% trans 'Invitation' %}{% endblock %}
{% block byline %}{% trans 'Please fill in your invitation details' %}{% endblock %}
......@@ -12,9 +13,25 @@
{% endblock %}
{% block content %}
<form method="post" action=".">
<form id="register_invitee" method="post" action=".">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans 'Register >>' %}" />
</form>
<div id="spinner-wrap" class="hidden">
<div class="spinner"></div>
<p class="wait">Please wait ...</p>
</div>
{% endblock %}
{% block extra_js %}
<script>
const frm = document.getElementById('register_invitee'),
spin = document.getElementById('spinner-wrap');
frm.addEventListener('submit', enable_spinner);
function enable_spinner() {
spin.setAttribute('class', 'spinner-wrapper');
}
</script>
{% endblock %}
......@@ -2,22 +2,15 @@ from django.conf import settings
from django.contrib.auth import logout
from django.http import Http404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from captcha.fields import CaptchaField
from invitation.models import Invitation
from ldapregister.forms import RegistrationForm
from registration.backends.simple.views import RegistrationView
from purist.models import AccountType
class InvitationRegistrationForm(RegistrationForm):
captcha = CaptchaField(
label=_('Please solve this sum'),
help_text=_('Prove you are not a robot. '
'Solve this addition, subtraction or multiplication '
'problem.')
)
from cart.views import CartRegistrationFormWithCaptcha as \
InvitationRegistrationForm
from limitmonitor.task_resources.subscription import \
create_invitee_subscription
from limitmonitor.common import forced_update
class InvitationRegistrationView(RegistrationView):
......@@ -73,38 +66,9 @@ class InvitationRegistrationView(RegistrationView):
self.invitation.guest = user
self.invitation.save()
# Active same limists/services as the owner
user_limits = user.limit_set.all()
owner_limits = self.invitation.owner.limit_set.all()
for o_limit in owner_limits:
o_service = o_limit.service
if o_service != settings.LM_SERVICES.GROUP:
for u_limit in user_limits:
if u_limit.service == o_service:
u_limit.renewal_date = o_limit.renewal_date
u_limit.expiry_date = o_limit.expiry_date
u_limit.volume_total = o_limit.volume_total
u_limit.time_total = o_limit.time_total
u_limit.is_active = o_limit.is_active
u_limit.created_date = o_limit.created_date
u_limit.updated_date = o_limit.updated_date
# Tunnel activation requires manual
# action from user
if (
o_service == settings.LM_SERVICES.TUNNEL and
o_limit.remaining_use_time().total_seconds() > 0
):
u_limit.is_active = False
u_limit.save()
# Make new user member of same groups as owner
if o_limit.is_active:
if o_service == settings.LM_SERVICES.SOCIAL:
user.set_ldap_group('social')
elif o_service == settings.LM_SERVICES.CHAT:
user.set_ldap_group('chat')
elif o_service == settings.LM_SERVICES.MAIL:
user.set_ldap_group('mail')
elif o_service == settings.LM_SERVICES.XMPP:
user.set_ldap_group('xmpp')
# Create an Invitee subscritpion for the user
sub = create_invitee_subscription(user)
if sub:
forced_update(user)
return user
......@@ -3,11 +3,12 @@ import datetime
from django.conf import settings
from django.utils import timezone
from django.contrib.auth.hashers import make_password
from purist.models import get_woo_connection, AccountType
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
from limitmonitor.models import ExternalCredit, ExternalBundle, Limit, \
get_available_bundles
from limitmonitor.tunnel import TunnelManager
import strictyaml
logger = logging.getLogger(__name__)
......@@ -94,6 +95,8 @@ def parse_subscription(json_entry):
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:
......@@ -119,21 +122,6 @@ def parse_subscription(json_entry):
return result
def get_user_subscriptions(user):
user_wc_id = user.get_woocommerce_id()
if user_wc_id is None:
# The user does not have WC account
return None
woo = get_woo_connection()
subscriptions = woo.get(
'subscriptions?customer={}'.format(user_wc_id)).json()
if len(subscriptions) >= 1:
return subscriptions
else:
return None
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
......@@ -179,8 +167,22 @@ def get_services_from_bundle(bundle_key):
return result
def update_services(user, srv_to_disable, srv_to_enable, renewal_date):
# Disable services
for srvn in srv_to_disable:
serv = user.limit_set.get(service=srvn)
serv.disable()
serv.save()
# Enable services
for srvn in srv_to_enable:
serv = user.limit_set.get(service=srvn)
serv.enable(renewal_date)
serv.save()
def forced_update(user):
subscriptions = get_user_subscriptions(user)
subscriptions = user.get_woocommerce_subscriptions()
if subscriptions is None:
logger.info('User {} has no subscriptions.'.format(user.username))
return
......@@ -214,18 +216,9 @@ def forced_update(user):
is_pending = False
is_converted = True
try:
# Disable services
for srvn in srv_to_disable:
serv = user.limit_set.get(service=srvn)
serv.disable()
serv.save()
# Enable services
for srvn in srv_to_enable | srv_to_reenable:
serv = user.limit_set.get(service=srvn)
serv.enable(parsed_sub['next_renewal'])
serv.save()
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
......@@ -236,8 +229,8 @@ def forced_update(user):
error = '{}: {}'.format(ext_cred.external_key, repr(e2))
finally:
if hasattr(user, 'chosenreward'):
user.chosenreward.is_pending = is_pending
user.chosenreward.save()
user.chosenreward.is_pending = is_pending
user.chosenreward.save()
ext_cred.error_message = error
ext_cred.is_converted = is_converted
ext_cred.save()
......@@ -313,7 +306,7 @@ def delete_account(user, purge_n=0, purge=False):
user.save()
# Cancell all user subscriptions
subs = get_user_subscriptions(user)
subs = user.get_woocommerce_subscriptions()
if subs:
for sub in subs:
sub_id = sub.get('id', None)
......@@ -333,8 +326,8 @@ def delete_account(user, purge_n=0, purge=False):
def get_users_expired_subs(date_limit=None):
"""Returns the list of users for which the service renewal_date is
older than date_limit. If date_limit is not specified
timezone.now() will be used. It will omit invited users, inactive
users and users created with magic carts.
timezone.now() will be used. It will omit inactive users and users
created with magic carts.
"""
magic_carts = [0, 999, 5000, 5999]
......@@ -356,7 +349,6 @@ def get_users_expired_subs(date_limit=None):
if (
user not in users and user.is_active
and user.account_type != AccountType.INVITED
and not is_magic
):
users.append(user)
......@@ -413,39 +405,3 @@ def set_subscription_status(subscription_id, status):
if r_status2 == status:
return True
return False
def get_user_external_credit(user):
ecs = ExternalCredit.objects.filter(
account_name=user.get_identity()
).order_by('-updated_date')
if len(ecs) >= 1:
return ecs[0]
return None
def get_available_bundles():
with open(settings.WOO_BUNDLES_INFO_FILE, 'r') as stream:
bundles = strictyaml.load(stream.read()).data
return bundles
def get_user_upgrade_choices(user, add_downgrade=False):
choices = []
ec = get_user_external_credit(user)
if ec is None:
return choices
bundles = get_available_bundles()
cur_bundle = bundles.get(ec.bundle_key, None)
if cur_bundle is None:
return choices
for upgrade in cur_bundle['upgrades']:
choices.append((upgrade, bundles[upgrade]['title']))
if add_downgrade:
for downgrade in cur_bundle['downgrades']:
choices.append((downgrade, bundles[downgrade]['title']))
return choices
......@@ -2,6 +2,7 @@ from choicesenum import ChoicesEnum
from django.conf import settings
from django.db import models
from django.utils import timezone
import strictyaml
class Legacy(ChoicesEnum):
......@@ -238,3 +239,10 @@ class ExternalCredit(models.Model):
@property
def external_bundle(self):
return self.parser_name + ":" + str(self.external_key)
def get_available_bundles():
with open(settings.WOO_BUNDLES_INFO_FILE, 'r') as stream:
bundles = strictyaml.load(stream.read()).data
return bundles
from django.conf import settings
from django.utils import timezone
from celery.utils.log import get_task_logger
from cart.models import ChosenReward
from purist.models import AccountType
from bundlechange.models import BundleChange
from limitmonitor.common import parse_subscription, get_user_subscriptions, \
from invitation.models import Invitation
from limitmonitor.models import get_available_bundles
from limitmonitor.common import parse_subscription, SubscriptionParseError, \
subscription_was_processed, get_services_from_bundle, forced_update, \
get_users_expired_subs, order_paid, set_subscription_status, \
SubscriptionParseError
get_users_expired_subs, order_paid, set_subscription_status
logger = get_task_logger(__name__)
def process_pending_registrations_user(user):
subscriptions = get_user_subscriptions(user)
subscriptions = user.get_woocommerce_subscriptions()
if subscriptions is None:
logger.info('User {} has no subscriptions.'.format(user.username))
......@@ -88,10 +91,11 @@ def process_renewals():
def upgrade_user_subscription(user, variation_id,
product_id=settings.WOO_PARENT_PROD_ID):
"""Given a user and a variation_id creates a new WooCommerce order
with a subscriptions associated to it. The item in the
subscription will be the product variation specified by
variation_id and product_id. By default product_id is the default
product configured in LDH.
with a subscription associated to it. The billing and shipping
information are taken from the user previous subscritpion. The
item in the new subscription will be the product variation
specified by variation_id and product_id. By default product_id is
the default product configured in LDH.
Returns a dictionary containing:
......@@ -101,8 +105,9 @@ def upgrade_user_subscription(user, variation_id,
create the payment link.
* subscription: The created subscription in JSON format.
"""
subs = get_user_subscriptions(user)
subs = user.get_woocommerce_subscriptions()
if subs is None:
return
order_data = {
......@@ -142,7 +147,7 @@ def process_bundle_changes():
is_paid = order_paid(pending_change.new_order_id)
# Check if the subscription is active
subs = get_user_subscriptions(pending_change.user)
subs = pending_change.user.get_woocommerce_subscriptions()
if subs is None:
logger.info('User {} has no subscriptions. BundleChange cannot be '
'processed'.format(pending_change.user))
......@@ -164,5 +169,149 @@ def process_bundle_changes():
set_subscription_status(pending_change.old_subscription_id,
'cancelled')
forced_update(pending_change.user)
# if this is a Family pack downgrade deal with the
# invitees
if pending_change.is_family_downgrade():
downgrade_invitees(pending_change.user)
pending_change.is_pending = False
pending_change.save()
def downgrade_invitees(user):
invs = user.get_invitees()
for inv in invs:
# Create Basic monthly subscription if the invitee has a
# recovery email
if inv.email != '':
# get first active subscription that should be an Invitee
# subscription
i_subs = inv.get_woocommerce_subscriptions()
if i_subs:
p_sub = parse_subscription(i_subs[0])
if (
p_sub['status'] == 'active' and
p_sub['account_type'] == AccountType.INVITED
):
# cancell invitee old subs
set_subscription_status(p_sub['external_key'], 'cancelled')
# create new Basic subs
order_info = create_user_subscription(inv, price=0,
paid=True)
if order_info.get('id', None) is not None:
forced_update(inv)
# remove the invitation
invitation = user.invitation_set.get(guest=inv)
invitation.delete()
else:
logger.info('Invitee {} current active subscription is '
'not of type INVITED'.format(inv.username))
else:
logger.info('Invitee {} does not have subscriptions'.format(
inv.username))
else:
logger.info('Could not downgrade invitee {} to Basic account due '
'to not having recovery email'.format(inv.username))
def create_user_subscription(user, bundle_type='BASIC',
billing_period='monthly',
price=None, paid=False, next_payment_date=None,
billing_email=None,
product_id=settings.WOO_PARENT_PROD_ID):
"""Creates a new WooCommerce order with a subscription for the user
using bundle_type, billing_period, price, etc as specified by
parameters. Default values if not specified will be:
bundle_type = BASIC
billing_period = monthly
price = as provided by WC
paid = False
next_payment_date = 30 days from now
billing_email = email set by WC
product_id = As configured in LDH
Returns a dictionary containing:
* id: This is the order id.
* key: This is the order_key for the order. This can be used to
create the payment link.
* subscription: The created subscription in JSON format.
"""
# Find variation_id
bundles = get_available_bundles()
variation_id = None
for k, v in bundles.items():
if v['type'] == bundle_type and v['period'] == billing_period:
variation_id = k
break
user_wc_id = user.get_woocommerce_id()
if next_payment_date is None:
next_payment_date = timezone.now() + timezone.timedelta(days=30)
if price is None:
price = ''
else:
price = str(price)
order_data = {
"payment_method": "stripe",
"payment_method_title": "Credit Card (Stripe)",
"set_paid": paid,
"line_items": [
{
"product_id": product_id,
"variation_id": variation_id,
"quantity": 1,
"price": price
}
],
}
if billing_email:
order_data['billing'] = {'email': billing_email}
order_data['customer_id'] = user_wc_id
order = user.woo_post_json('orders', order_data)
subs_data = {
"line_items": [
{
"product_id": product_id,
"variation_id": variation_id,
"quantity": 1,
}
],
"next_payment_date": next_payment_date.strftime('%Y-%m-%d %H:%M:%S'),
}
subs_data['parent_id'] = order['id']
subs_data['customer_id'] = order['customer_id']
subs_data['status'] = 'active'
if billing_email:
subs_data['billing'] = order_data['billing']
sub_order = user.woo_post_json('subscriptions', subs_data)
return {'id': order['id'],
'key': order['order_key'],
'subscription': sub_order}
def create_invitee_subscription(user):
result = None
try:
invitation = Invitation.objects.get(guest=user)
except Invitation.DoesNotExist:
return result