Skip to content

Commit

Permalink
extra usage
Browse files Browse the repository at this point in the history
  • Loading branch information
knivets committed Sep 17, 2018
1 parent 3622acc commit 64afb15
Show file tree
Hide file tree
Showing 3 changed files with 267 additions and 29 deletions.
192 changes: 192 additions & 0 deletions saas/migrations/0008_auto_20180907_1031.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-09-07 15:31
from __future__ import unicode_literals

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('saas', '0007_0_3_3'),
]

operations = [
migrations.AddField(
model_name='usecharge',
name='quota',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='balanceline',
name='selector',
field=models.CharField(blank=True, max_length=255),
),
migrations.AlterField(
model_name='charge',
name='amount',
field=models.PositiveIntegerField(default=0, help_text='total amount in cents (i.e. 100ths) of unit'),
),
migrations.AlterField(
model_name='charge',
name='created_at',
field=models.DateTimeField(help_text='date/time of creation in ISO format'),
),
migrations.AlterField(
model_name='charge',
name='description',
field=models.TextField(help_text='description for the Charge as appears on billing statements', null=True),
),
migrations.AlterField(
model_name='charge',
name='exp_date',
field=models.DateField(help_text='expiration date of the credit card used'),
),
migrations.AlterField(
model_name='charge',
name='last4',
field=models.PositiveSmallIntegerField(help_text='last 4 digits of the credit card used'),
),
migrations.AlterField(
model_name='charge',
name='state',
field=models.PositiveSmallIntegerField(choices=[(1, 'done'), (3, 'disputed'), (2, 'failed'), (0, 'created')], default=0, help_text="current state ('created', 'done', 'failed', 'disputed')"),
),
migrations.AlterField(
model_name='charge',
name='unit',
field=models.CharField(default='usd', help_text='three-letter ISO 4217 code for currency unit (ex: usd)', max_length=3),
),
migrations.AlterField(
model_name='coupon',
name='code',
field=models.SlugField(help_text='unique identifier per provider, typically used in URLs'),
),
migrations.AlterField(
model_name='coupon',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='date/time of creation in ISO format'),
),
migrations.AlterField(
model_name='coupon',
name='description',
field=models.TextField(blank=True, help_text='free-form text description for the Coupon', null=True),
),
migrations.AlterField(
model_name='coupon',
name='ends_at',
field=models.DateTimeField(blank=True, help_text='date/time in ISO format at which the code expires to purchase subscriptions', null=True),
),
migrations.AlterField(
model_name='coupon',
name='nb_attempts',
field=models.IntegerField(blank=True, help_text='number of times the coupon can be used', null=True),
),
migrations.AlterField(
model_name='coupon',
name='organization',
field=models.ForeignKey(help_text='coupon will only apply to purchased plans from this provider', on_delete=django.db.models.deletion.CASCADE, to='saas.Organization'),
),
migrations.AlterField(
model_name='coupon',
name='percent',
field=models.PositiveSmallIntegerField(default=0, help_text='percentage discounted', validators=[django.core.validators.MaxValueValidator(100)]),
),
migrations.AlterField(
model_name='coupon',
name='plan',
field=models.ForeignKey(blank=True, help_text='coupon will only apply to this plan', null=True, on_delete=django.db.models.deletion.CASCADE, to='saas.Plan'),
),
migrations.AlterField(
model_name='organization',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='Date/time of creation in ISO format'),
),
migrations.AlterField(
model_name='organization',
name='default_timezone',
field=models.CharField(default='America/Chicago', max_length=100),
),
migrations.AlterField(
model_name='organization',
name='full_name',
field=models.CharField(blank=True, max_length=100, verbose_name='Organization name'),
),
migrations.AlterField(
model_name='organization',
name='postal_code',
field=models.CharField(max_length=50, verbose_name='Zip/Postal Code'),
),
migrations.AlterField(
model_name='roledescription',
name='created_at',
field=models.DateTimeField(auto_now_add=True, help_text='Date/time of creation in ISO format'),
),
migrations.AlterField(
model_name='roledescription',
name='slug',
field=models.SlugField(help_text='Unique identifier, typically used in URLs.'),
),
migrations.AlterField(
model_name='roledescription',
name='title',
field=models.CharField(help_text='Short description of the role. Grammatical rules to pluralize the title might be used in User Interfaces.', max_length=20),
),
migrations.AlterField(
model_name='transaction',
name='created_at',
field=models.DateTimeField(help_text='date/time of creation in ISO format'),
),
migrations.AlterField(
model_name='transaction',
name='descr',
field=models.TextField(default='N/A', help_text='free-form text description for the Transaction'),
),
migrations.AlterField(
model_name='transaction',
name='dest_account',
field=models.CharField(default='unknown', help_text='target account to which funds are deposited', max_length=255),
),
migrations.AlterField(
model_name='transaction',
name='dest_amount',
field=models.PositiveIntegerField(default=0, help_text='amount deposited into target in dest_unit'),
),
migrations.AlterField(
model_name='transaction',
name='dest_organization',
field=models.ForeignKey(help_text='target Organization to which funds are deposited', on_delete=django.db.models.deletion.PROTECT, related_name='incoming', to='saas.Organization'),
),
migrations.AlterField(
model_name='transaction',
name='dest_unit',
field=models.CharField(default='usd', help_text='three-letter ISO 4217 code for target currency unit (ex: usd)', max_length=3),
),
migrations.AlterField(
model_name='transaction',
name='event_id',
field=models.SlugField(help_text='Event at the origin of this transaction" " (ex. subscription, charge, etc.)', null=True),
),
migrations.AlterField(
model_name='transaction',
name='orig_account',
field=models.CharField(default='unknown', help_text='source account from which funds are withdrawn', max_length=255),
),
migrations.AlterField(
model_name='transaction',
name='orig_amount',
field=models.PositiveIntegerField(default=0, help_text='amount withdrawn from source in orig_unit'),
),
migrations.AlterField(
model_name='transaction',
name='orig_organization',
field=models.ForeignKey(help_text='source Organization from which funds are withdrawn', on_delete=django.db.models.deletion.PROTECT, related_name='outgoing', to='saas.Organization'),
),
migrations.AlterField(
model_name='transaction',
name='orig_unit',
field=models.CharField(default='usd', help_text='three-letter ISO 4217 code for source currency unit (ex: usd)', max_length=3),
),
]
47 changes: 33 additions & 14 deletions saas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3323,6 +3323,16 @@ def get_subscription_income_balance(self, subscription,
return self.get_event_balance(subscription.id,
account=Transaction.INCOME, starts_at=starts_at, ends_at=ends_at)

def get_use_charge_balance(self, subscription, use_charge,
starts_at=None, ends_at=None):
"""
Returns the recognized income balance on a use charge
for the period [starts_at, ends_at[ as a tuple (amount, unit).
"""
event_id = get_sub_event_id(subscription, use_charge)
return self.get_event_balance(event_id,
account=Transaction.INCOME, starts_at=starts_at, ends_at=ends_at)

def get_subscription_invoiceables(self, subscription, until=None):
"""
Returns a set of payable or liability ``Transaction`` since
Expand Down Expand Up @@ -3364,7 +3374,7 @@ def get_subscription_receivable(self, subscription,
created_at__lt=until, **kwargs).order_by('created_at')

def new_use_charge(self, subscription, use_charge, quantity,
created_at=None, descr=None):
custom_amount=None, created_at=None, descr=None):
"""
Each time a subscriber places an order through
the /billing/:organization/cart/ page, a ``Transaction``
Expand All @@ -3384,7 +3394,10 @@ def new_use_charge(self, subscription, use_charge, quantity,
if quantity <= 0:
# Minimum quantity for a use charge is one.
quantity = 1
amount = use_charge.use_amount * quantity
if custom_amount is not None:
amount = custom_amount * quantity
else:
amount = use_charge.use_amount * quantity
event_id = get_sub_event_id(subscription, use_charge)
if not descr:
descr = humanize.describe_buy_use(use_charge, quantity)
Expand Down Expand Up @@ -3540,7 +3553,8 @@ def create_period_started(self, subscription, created_at=None):
event_id=subscription.id)

def create_income_recognized(self, subscription,
amount=0, starts_at=None, ends_at=None, descr=None, dry_run=False):
amount=0, starts_at=None, ends_at=None, descr=None,
event_id=None, dry_run=False):
"""
When a period ends and we either have a ``Backlog`` (payment
was made before the period starts) or a ``Receivable`` (invoice
Expand All @@ -3564,14 +3578,16 @@ def create_income_recognized(self, subscription,
#pylint:disable=unused-argument,too-many-arguments,too-many-locals
created_transactions = []
ends_at = datetime_or_now(ends_at)
if not event_id:
event_id = subscription.id
# ``created_at`` is set just before ``ends_at``
# so we do not include the newly created transaction
# in the subsequent period.
created_at = ends_at - relativedelta(seconds=1)
balance = self.get_event_balance(subscription.id,
balance = self.get_event_balance(event_id,
account=Transaction.BACKLOG, ends_at=ends_at)
backlog_amount = - balance['amount'] # def. balance must be negative
balance = self.get_event_balance(subscription.id,
balance = self.get_event_balance(event_id,
account=Transaction.RECEIVABLE, ends_at=ends_at)
receivable_amount = - balance['amount'] # def. balance must be negative
LOGGER.debug("recognize %dc(%s) with %dc(%s) backlog available,"\
Expand All @@ -3592,7 +3608,7 @@ def create_income_recognized(self, subscription,
recognized = Transaction(
created_at=created_at,
descr=descr,
event_id=subscription.id,
event_id=event_id,
dest_amount=available,
dest_unit=subscription.plan.unit,
dest_account=Transaction.BACKLOG,
Expand All @@ -3618,7 +3634,7 @@ def create_income_recognized(self, subscription,
recognized = Transaction(
created_at=created_at,
descr=descr,
event_id=subscription.id,
event_id=event_id,
dest_amount=available,
dest_unit=subscription.plan.unit,
dest_account=Transaction.RECEIVABLE,
Expand Down Expand Up @@ -3959,20 +3975,23 @@ def sum_orig_amount(transactions):

def get_period_usage(subscription, use_charge, starts_at, ends_at):
return Transaction.objects.filter(
orig_account=Transaction.RECEIVABLE, dest_account=Transaction.PAYABLE,
created_at__lt=ends_at,
orig_account=Transaction.RECEIVABLE,
dest_account=Transaction.PAYABLE, created_at__lt=ends_at,
created_at__gte=starts_at,
event_id=get_sub_event_id(subscription, use_charge)).count()

def record_use_charge(subscription, use_charge):
usage = get_period_usage(subscription, use_charge,
subscription.created_at, subscription.ends_at)
amount = 0
if usage > use_charge.quota:
amount = 1
amount = None
event_id = get_sub_event_id(subscription, use_charge)
descr = event_id
if usage < use_charge.quota:
amount = 0
descr = '%s (complimentary in plan)' % event_id
return Transaction.objects.record_order([
Transaction.objects.new_use_charge(
subscription, use_charge, amount)])
Transaction.objects.new_use_charge(subscription,
use_charge, 1, custom_amount=amount, descr=descr)])

def get_sub_event_id(subscription, use_charge=None):
substr = "sub_%d" % subscription.id
Expand Down
57 changes: 42 additions & 15 deletions saas/renewals.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

from . import humanize, signals
from .models import (Charge, Organization, Plan, Subscription, Transaction,
sum_dest_amount, get_period_usage)
sum_dest_amount, get_period_usage, get_sub_event_id)
from .utils import datetime_or_now

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -117,25 +117,52 @@ def _recognize_subscription_income(subscription, until=None):
'period_start': descr_period_start,
'period_end': descr_period_end})

# recognizing use charges for subscription
use_charges = subscription.plan.use_charges.all()
for use_charge in use_charges:
quantity = get_period_usage(subscription, use_charge,
recognize_start, recognize_end)
extra = quantity - use_charge.quota
to_recognize_amount = 0
if extra > 0:
to_recognize_amount = extra * use_charge.use_amount

balance = Transaction.objects.get_use_charge_balance(
subscription, use_charge, recognize_start, recognize_end)
recognized_amount = balance['amount']
recognized_amount = abs(recognized_amount)

if to_recognize_amount > recognized_amount:
amount = to_recognize_amount - recognized_amount
event_id = get_sub_event_id(subscription, use_charge)

# creating a liability for a customer
Transaction.objects.create(
event_id=event_id,
created_at=recognize_end - relativedelta(seconds=1),
descr=event_id,
dest_unit=subscription.plan.unit,
dest_amount=amount,
dest_account=Transaction.LIABILITY,
dest_organization=subscription.organization,
orig_unit=subscription.plan.unit,
orig_amount=amount,
orig_account=Transaction.PAYABLE,
orig_organization=subscription.organization)

# recognizing an income for a provider
descr = "%s income recognized" % event_id
Transaction.objects.create_income_recognized(
subscription, amount=amount, event_id=event_id,
starts_at=recognize_start, ends_at=recognize_end,
descr=descr)

recognize_period_idx += 1
recognize_start = (subscription.created_at
+ relativedelta(months=recognize_period_idx))
recognize_end = (subscription.created_at
+ relativedelta(months=recognize_period_idx + 1))
use_charges = subscription.plan.use_charges.all()
use_charge_amount = 0
for use_charge in use_charges:
quantity = get_period_usage(subscription, use_charge,
recognize_start, recognize_end)
extra = quantity - use_charge.quota
if extra > 0:
use_charge_amount += extra * use_charge.use_amount
if use_charge_amount > 0:
descr = "use charge for plan %d" % subscription.plan.id
Transaction.objects.create_income_recognized(
subscription, amount=use_charge_amount,
starts_at=recognize_start, ends_at=recognize_end,
descr=descr)

order_subscribe_beg = order_subscribe_end
if recognize_end >= until:
break
Expand Down

0 comments on commit 64afb15

Please sign in to comment.