Add utils and tests for calculating membership status
This commit is contained in:
parent
f9c21b7001
commit
0fe999ca97
|
@ -36,6 +36,7 @@ class Member(models.Model):
|
||||||
current_start_date = models.DateField(default=date.today, null=True)
|
current_start_date = models.DateField(default=date.today, null=True)
|
||||||
application_date = models.DateField(default=date.today, null=True)
|
application_date = models.DateField(default=date.today, null=True)
|
||||||
vetted_date = models.DateField(blank=True, null=True)
|
vetted_date = models.DateField(blank=True, null=True)
|
||||||
|
paused_date = models.DateField(blank=True, null=True)
|
||||||
monthly_fees = models.IntegerField(default=55, blank=True, null=True)
|
monthly_fees = models.IntegerField(default=55, blank=True, null=True)
|
||||||
|
|
||||||
class Transaction(models.Model):
|
class Transaction(models.Model):
|
||||||
|
|
|
@ -1,3 +1,343 @@
|
||||||
from django.test import TestCase
|
import django, sys, os
|
||||||
|
os.environ['DJANGO_SETTINGS_MODULE'] = 'apiserver.settings'
|
||||||
|
django.setup()
|
||||||
|
|
||||||
# Create your tests here.
|
from django.test import TestCase
|
||||||
|
import datetime
|
||||||
|
from dateutil import relativedelta
|
||||||
|
|
||||||
|
from apiserver.api import utils, models
|
||||||
|
|
||||||
|
testing_member, _ = models.Member.objects.get_or_create(
|
||||||
|
first_name='unittest',
|
||||||
|
preferred_name='unittest',
|
||||||
|
last_name='tester',
|
||||||
|
)
|
||||||
|
|
||||||
|
class TestMonthsSpanned(TestCase):
|
||||||
|
def test_num_months_spanned_one_month(self):
|
||||||
|
date2 = datetime.date(2020, 1, 10)
|
||||||
|
date1 = datetime.date(2020, 2, 10)
|
||||||
|
|
||||||
|
spanned = utils.num_months_spanned(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(spanned, 1)
|
||||||
|
|
||||||
|
def test_num_months_spanned_one_week(self):
|
||||||
|
date1 = datetime.date(2020, 2, 5)
|
||||||
|
date2 = datetime.date(2020, 1, 28)
|
||||||
|
|
||||||
|
spanned = utils.num_months_spanned(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(spanned, 1)
|
||||||
|
|
||||||
|
def test_num_months_spanned_two_days(self):
|
||||||
|
date1 = datetime.date(2020, 2, 1)
|
||||||
|
date2 = datetime.date(2020, 1, 31)
|
||||||
|
|
||||||
|
spanned = utils.num_months_spanned(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(spanned, 1)
|
||||||
|
|
||||||
|
def test_num_months_spanned_two_years(self):
|
||||||
|
date1 = datetime.date(2022, 1, 18)
|
||||||
|
date2 = datetime.date(2020, 1, 18)
|
||||||
|
|
||||||
|
spanned = utils.num_months_spanned(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(spanned, 24)
|
||||||
|
|
||||||
|
def test_num_months_spanned_same_month(self):
|
||||||
|
date1 = datetime.date(2020, 1, 31)
|
||||||
|
date2 = datetime.date(2020, 1, 1)
|
||||||
|
|
||||||
|
spanned = utils.num_months_spanned(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(spanned, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMonthsDifference(TestCase):
|
||||||
|
def test_num_months_difference_one_month(self):
|
||||||
|
date2 = datetime.date(2020, 1, 10)
|
||||||
|
date1 = datetime.date(2020, 2, 10)
|
||||||
|
|
||||||
|
difference = utils.num_months_difference(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(difference, 1)
|
||||||
|
|
||||||
|
def test_num_months_difference_one_week(self):
|
||||||
|
date1 = datetime.date(2020, 2, 5)
|
||||||
|
date2 = datetime.date(2020, 1, 28)
|
||||||
|
|
||||||
|
difference = utils.num_months_difference(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(difference, 0)
|
||||||
|
|
||||||
|
def test_num_months_difference_two_days(self):
|
||||||
|
date1 = datetime.date(2020, 2, 1)
|
||||||
|
date2 = datetime.date(2020, 1, 31)
|
||||||
|
|
||||||
|
difference = utils.num_months_difference(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(difference, 0)
|
||||||
|
|
||||||
|
def test_num_months_difference_two_years(self):
|
||||||
|
date1 = datetime.date(2022, 1, 18)
|
||||||
|
date2 = datetime.date(2020, 1, 18)
|
||||||
|
|
||||||
|
difference = utils.num_months_difference(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(difference, 24)
|
||||||
|
|
||||||
|
def test_num_months_difference_same_month(self):
|
||||||
|
date1 = datetime.date(2020, 1, 31)
|
||||||
|
date2 = datetime.date(2020, 1, 1)
|
||||||
|
|
||||||
|
difference = utils.num_months_difference(date1, date2)
|
||||||
|
|
||||||
|
self.assertEqual(difference, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddMonths(TestCase):
|
||||||
|
def test_add_months_one_month(self):
|
||||||
|
date = datetime.date(2020, 1, 18)
|
||||||
|
num_months = 1
|
||||||
|
|
||||||
|
new_date = utils.add_months(date, num_months)
|
||||||
|
|
||||||
|
self.assertEqual(new_date, datetime.date(2020, 2, 18))
|
||||||
|
|
||||||
|
def test_add_months_february(self):
|
||||||
|
date = datetime.date(2020, 1, 31)
|
||||||
|
num_months = 1
|
||||||
|
|
||||||
|
new_date = utils.add_months(date, num_months)
|
||||||
|
|
||||||
|
self.assertEqual(new_date, datetime.date(2020, 2, 29))
|
||||||
|
|
||||||
|
def test_add_months_february_leap(self):
|
||||||
|
date = datetime.date(2020, 2, 29)
|
||||||
|
num_months = 12
|
||||||
|
|
||||||
|
new_date = utils.add_months(date, num_months)
|
||||||
|
|
||||||
|
self.assertEqual(new_date, datetime.date(2021, 2, 28))
|
||||||
|
|
||||||
|
def test_add_months_hundred_years(self):
|
||||||
|
date = datetime.date(2020, 1, 31)
|
||||||
|
num_months = 1200
|
||||||
|
|
||||||
|
new_date = utils.add_months(date, num_months)
|
||||||
|
|
||||||
|
self.assertEqual(new_date, datetime.date(2120, 1, 31))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCalcStatus(TestCase):
|
||||||
|
def test_calc_member_status_14_days(self):
|
||||||
|
expire_date = datetime.date.today() + datetime.timedelta(days=14)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Current')
|
||||||
|
|
||||||
|
def test_calc_member_status_90_days(self):
|
||||||
|
expire_date = datetime.date.today() + datetime.timedelta(days=90)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Prepaid')
|
||||||
|
|
||||||
|
def test_calc_member_status_tomorrow(self):
|
||||||
|
expire_date = datetime.date.today() + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Current')
|
||||||
|
|
||||||
|
def test_calc_member_status_today(self):
|
||||||
|
expire_date = datetime.date.today()
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Current')
|
||||||
|
|
||||||
|
def test_calc_member_status_yesterday(self):
|
||||||
|
expire_date = datetime.date.today() - datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Due')
|
||||||
|
|
||||||
|
def test_calc_member_status_85_days_ago(self):
|
||||||
|
expire_date = datetime.date.today() - datetime.timedelta(days=85)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Overdue')
|
||||||
|
|
||||||
|
def test_calc_member_status_95_days_ago(self):
|
||||||
|
expire_date = datetime.date.today() - datetime.timedelta(days=95)
|
||||||
|
|
||||||
|
status = utils.calc_member_status(expire_date)
|
||||||
|
|
||||||
|
self.assertEqual(status, 'Former Member')
|
||||||
|
|
||||||
|
|
||||||
|
class TestFakeMonths(TestCase):
|
||||||
|
def test_fake_missing_membership_months_one_month(self):
|
||||||
|
testing_member.current_start_date = datetime.date(2018, 6, 6)
|
||||||
|
testing_member.expire_date = datetime.date(2018, 7, 6)
|
||||||
|
|
||||||
|
tx = utils.fake_missing_membership_months(testing_member)
|
||||||
|
|
||||||
|
self.assertEqual(tx.number_of_membership_months, 1)
|
||||||
|
|
||||||
|
def test_fake_missing_membership_months_one_and_half_month(self):
|
||||||
|
testing_member.current_start_date = datetime.date(2018, 6, 1)
|
||||||
|
testing_member.expire_date = datetime.date(2018, 7, 15)
|
||||||
|
|
||||||
|
tx = utils.fake_missing_membership_months(testing_member)
|
||||||
|
|
||||||
|
self.assertEqual(tx.number_of_membership_months, 1)
|
||||||
|
|
||||||
|
def test_fake_missing_membership_months_one_year(self):
|
||||||
|
testing_member.current_start_date = datetime.date(2018, 6, 6)
|
||||||
|
testing_member.expire_date = datetime.date(2019, 6, 6)
|
||||||
|
|
||||||
|
tx = utils.fake_missing_membership_months(testing_member)
|
||||||
|
|
||||||
|
self.assertEqual(tx.number_of_membership_months, 12)
|
||||||
|
|
||||||
|
def test_fake_missing_membership_months_same_month(self):
|
||||||
|
testing_member.current_start_date = datetime.date(2018, 6, 6)
|
||||||
|
testing_member.expire_date = datetime.date(2018, 6, 16)
|
||||||
|
|
||||||
|
tx = utils.fake_missing_membership_months(testing_member)
|
||||||
|
|
||||||
|
self.assertEqual(tx.number_of_membership_months, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTallyMembership(TestCase):
|
||||||
|
def get_member_clear_transactions(self):
|
||||||
|
member = testing_member
|
||||||
|
member.paused_date = None
|
||||||
|
member.expire_date = None
|
||||||
|
return member
|
||||||
|
|
||||||
|
def test_tally_membership_months_prepaid(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
test_num_months = 8
|
||||||
|
start_date = datetime.date.today() - relativedelta.relativedelta(months=6, days=14)
|
||||||
|
end_date = start_date + relativedelta.relativedelta(months=test_num_months)
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
for i in range(test_num_months):
|
||||||
|
models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
member_id=member.id,
|
||||||
|
number_of_membership_months=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(member.expire_date, end_date)
|
||||||
|
self.assertEqual(member.status, 'Prepaid')
|
||||||
|
|
||||||
|
def test_tally_membership_months_current(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
test_num_months = 7
|
||||||
|
start_date = datetime.date.today() - relativedelta.relativedelta(months=6, days=14)
|
||||||
|
end_date = start_date + relativedelta.relativedelta(months=test_num_months)
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
for i in range(test_num_months):
|
||||||
|
models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
member_id=member.id,
|
||||||
|
number_of_membership_months=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(member.expire_date, end_date)
|
||||||
|
self.assertEqual(member.status, 'Current')
|
||||||
|
|
||||||
|
def test_tally_membership_months_due(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
test_num_months = 6
|
||||||
|
start_date = datetime.date.today() - relativedelta.relativedelta(months=6, days=14)
|
||||||
|
end_date = start_date + relativedelta.relativedelta(months=test_num_months)
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
for i in range(test_num_months):
|
||||||
|
models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
member_id=member.id,
|
||||||
|
number_of_membership_months=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(member.expire_date, end_date)
|
||||||
|
self.assertEqual(member.status, 'Due')
|
||||||
|
|
||||||
|
def test_tally_membership_months_overdue(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
test_num_months = 5
|
||||||
|
start_date = datetime.date.today() - relativedelta.relativedelta(months=6, days=14)
|
||||||
|
end_date = start_date + relativedelta.relativedelta(months=test_num_months)
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
for i in range(test_num_months):
|
||||||
|
models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
member_id=member.id,
|
||||||
|
number_of_membership_months=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(member.expire_date, end_date)
|
||||||
|
self.assertEqual(member.status, 'Overdue')
|
||||||
|
|
||||||
|
def test_tally_membership_months_overdue(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
test_num_months = 1
|
||||||
|
start_date = datetime.date.today() - relativedelta.relativedelta(months=6, days=14)
|
||||||
|
end_date = start_date + relativedelta.relativedelta(months=test_num_months)
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
for i in range(test_num_months):
|
||||||
|
models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
member_id=member.id,
|
||||||
|
number_of_membership_months=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(member.expire_date, end_date)
|
||||||
|
self.assertEqual(member.paused_date, end_date)
|
||||||
|
self.assertEqual(member.status, 'Former Member')
|
||||||
|
|
||||||
|
def test_tally_membership_months_dont_run(self):
|
||||||
|
member = self.get_member_clear_transactions()
|
||||||
|
start_date = datetime.date.today()
|
||||||
|
|
||||||
|
member.current_start_date = start_date
|
||||||
|
member.paused_date = start_date
|
||||||
|
member.save()
|
||||||
|
|
||||||
|
result = utils.tally_membership_months(member)
|
||||||
|
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
96
apiserver/apiserver/api/utils.py
Normal file
96
apiserver/apiserver/api/utils.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import datetime
|
||||||
|
from dateutil import relativedelta
|
||||||
|
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from . import models, old_models
|
||||||
|
|
||||||
|
def num_months_spanned(d1, d2):
|
||||||
|
'''
|
||||||
|
Return number of months thresholds two dates span.
|
||||||
|
Order of arguments is same as subtraction
|
||||||
|
'''
|
||||||
|
return (d1.year - d2.year) * 12 + d1.month - d2.month
|
||||||
|
|
||||||
|
def num_months_difference(d1, d2):
|
||||||
|
'''
|
||||||
|
Return number of whole months between two dates.
|
||||||
|
Order of arguments is same as subtraction
|
||||||
|
'''
|
||||||
|
r = relativedelta.relativedelta(d1, d2)
|
||||||
|
return r.months + 12 * r.years
|
||||||
|
|
||||||
|
def calc_member_status(expire_date):
|
||||||
|
today = datetime.date.today()
|
||||||
|
difference = num_months_difference(expire_date, today)
|
||||||
|
|
||||||
|
if difference >= 1:
|
||||||
|
return 'Prepaid'
|
||||||
|
elif difference <= -3:
|
||||||
|
return 'Former Member'
|
||||||
|
elif difference <= -1:
|
||||||
|
return 'Overdue'
|
||||||
|
elif today <= expire_date:
|
||||||
|
return 'Current'
|
||||||
|
elif today > expire_date:
|
||||||
|
return 'Due'
|
||||||
|
else:
|
||||||
|
raise()
|
||||||
|
|
||||||
|
def add_months(date, num_months):
|
||||||
|
return date + relativedelta.relativedelta(months=num_months)
|
||||||
|
|
||||||
|
def fake_missing_membership_months(member):
|
||||||
|
'''
|
||||||
|
Return a transaction adding fake months on importing the member so the
|
||||||
|
length of their membership resolves to their imported expiry date
|
||||||
|
'''
|
||||||
|
start_date = member.current_start_date
|
||||||
|
expire_date = member.expire_date
|
||||||
|
|
||||||
|
missing_months = num_months_spanned(expire_date, start_date)
|
||||||
|
|
||||||
|
user = member.user if member.user else None
|
||||||
|
memo = '{} mth membership dues accounting old portal import, {} to {}'.format(
|
||||||
|
str(missing_months), start_date, expire_date
|
||||||
|
)
|
||||||
|
|
||||||
|
tx = models.Transaction.objects.create(
|
||||||
|
amount=0,
|
||||||
|
user=user,
|
||||||
|
memo=memo,
|
||||||
|
member_id=member.id,
|
||||||
|
reference_number='',
|
||||||
|
info_source='System',
|
||||||
|
payment_method='N/A',
|
||||||
|
category='Membership',
|
||||||
|
account_type='Clearing',
|
||||||
|
number_of_membership_months=missing_months,
|
||||||
|
)
|
||||||
|
|
||||||
|
return tx
|
||||||
|
|
||||||
|
def tally_membership_months(member):
|
||||||
|
'''
|
||||||
|
Sum together member's dues and calculate their new expire date and status
|
||||||
|
Doesn't work if member is paused.
|
||||||
|
'''
|
||||||
|
if member.paused_date: return False
|
||||||
|
|
||||||
|
start_date = member.current_start_date
|
||||||
|
|
||||||
|
txs = models.Transaction.objects.filter(member_id=member.id)
|
||||||
|
total_months_agg = txs.aggregate(Sum('number_of_membership_months'))
|
||||||
|
total_months = total_months_agg['number_of_membership_months__sum']
|
||||||
|
|
||||||
|
expire_date = add_months(start_date, total_months)
|
||||||
|
status = calc_member_status(expire_date)
|
||||||
|
|
||||||
|
member.expire_date = expire_date
|
||||||
|
member.status = status
|
||||||
|
|
||||||
|
if status == 'Former Member':
|
||||||
|
member.paused_date = expire_date
|
||||||
|
|
||||||
|
member.save()
|
||||||
|
return True
|
|
@ -29,7 +29,7 @@ export function LoginForm(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<Header size='medium'>Login to Spaceport</Header>
|
<Header size='medium'>Log In to Spaceport</Header>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='Username'
|
label='Username'
|
||||||
name='username'
|
name='username'
|
||||||
|
@ -45,7 +45,7 @@ export function LoginForm(props) {
|
||||||
error={error.password}
|
error={error.password}
|
||||||
/>
|
/>
|
||||||
<Form.Button loading={loading} error={error.non_field_errors}>
|
<Form.Button loading={loading} error={error.non_field_errors}>
|
||||||
Login
|
Log In
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,7 +32,7 @@ export function TransactionEditor(props) {
|
||||||
];
|
];
|
||||||
|
|
||||||
const sourceOptions = [
|
const sourceOptions = [
|
||||||
{ key: '0', text: 'Web', value: 'Web' },
|
{ key: '0', text: 'Web (Spaceport)', value: 'Web' },
|
||||||
{ key: '1', text: 'Database Edit', value: 'DB Edit' },
|
{ key: '1', text: 'Database Edit', value: 'DB Edit' },
|
||||||
{ key: '2', text: 'System', value: 'System' },
|
{ key: '2', text: 'System', value: 'System' },
|
||||||
{ key: '3', text: 'Receipt or Statement', value: 'Receipt or Stmt' },
|
{ key: '3', text: 'Receipt or Statement', value: 'Receipt or Stmt' },
|
||||||
|
|
Loading…
Reference in New Issue
Block a user