From 0fe999ca97b9b4e9d3490ef08f6eacadecf0ec3c Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sun, 19 Jan 2020 03:00:05 +0000 Subject: [PATCH] Add utils and tests for calculating membership status --- apiserver/apiserver/api/models.py | 1 + apiserver/apiserver/api/tests.py | 344 +++++++++++++++++++++++++++++- apiserver/apiserver/api/utils.py | 96 +++++++++ webclient/src/LoginSignup.js | 4 +- webclient/src/Transactions.js | 2 +- 5 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 apiserver/apiserver/api/utils.py diff --git a/apiserver/apiserver/api/models.py b/apiserver/apiserver/api/models.py index afadd33..2f6457b 100644 --- a/apiserver/apiserver/api/models.py +++ b/apiserver/apiserver/api/models.py @@ -36,6 +36,7 @@ class Member(models.Model): current_start_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) + paused_date = models.DateField(blank=True, null=True) monthly_fees = models.IntegerField(default=55, blank=True, null=True) class Transaction(models.Model): diff --git a/apiserver/apiserver/api/tests.py b/apiserver/apiserver/api/tests.py index 7ce503c..7c426db 100644 --- a/apiserver/apiserver/api/tests.py +++ b/apiserver/apiserver/api/tests.py @@ -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) diff --git a/apiserver/apiserver/api/utils.py b/apiserver/apiserver/api/utils.py new file mode 100644 index 0000000..fd3e2a1 --- /dev/null +++ b/apiserver/apiserver/api/utils.py @@ -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 diff --git a/webclient/src/LoginSignup.js b/webclient/src/LoginSignup.js index d0c9d7b..f4f41f6 100644 --- a/webclient/src/LoginSignup.js +++ b/webclient/src/LoginSignup.js @@ -29,7 +29,7 @@ export function LoginForm(props) { return (
-
Login to Spaceport
+
Log In to Spaceport
- Login + Log In ); diff --git a/webclient/src/Transactions.js b/webclient/src/Transactions.js index a95f782..2dbd57e 100644 --- a/webclient/src/Transactions.js +++ b/webclient/src/Transactions.js @@ -32,7 +32,7 @@ export function TransactionEditor(props) { ]; const sourceOptions = [ - { key: '0', text: 'Web', value: 'Web' }, + { key: '0', text: 'Web (Spaceport)', value: 'Web' }, { key: '1', text: 'Database Edit', value: 'DB Edit' }, { key: '2', text: 'System', value: 'System' }, { key: '3', text: 'Receipt or Statement', value: 'Receipt or Stmt' },