diff --git a/configuration/management/commands/add_config.py b/configuration/management/commands/add_config.py index 6beab368..23f14d45 100644 --- a/configuration/management/commands/add_config.py +++ b/configuration/management/commands/add_config.py @@ -314,6 +314,12 @@ class Command(BaseCommand): "heating": {"_label": "expenseOptions.heating", "_default_message": "Heating"}, "creditCard": {"_label": "expenseOptions.creditCard", "_default_message": "Credit Card Debt"}, "mortgage": {"_label": "expenseOptions.mortgage", "_default_message": "Mortgage"}, + "propertyTax": {"_label": "expenseOptions.propertyTax", "_default_message": "Property Taxes"}, + "hoa": {"_label": "expenseOptions.hoa", "_default_message": "Homeowners or Condo Association Fees and Dues"}, + "homeownersInsurance": { + "_label": "expenseOptions.homeownersInsurance", + "_default_message": "Homeowners Insurance", + }, "medical": {"_label": "expenseOptions.medical", "_default_message": "Medical Insurance Premium &/or Bills"}, "personalLoan": {"_label": "expenseOptions.personalLoan", "_default_message": "Personal Loan"}, "studentLoans": {"_label": "expenseOptions.studentLoans", "_default_message": "Student Loans"}, diff --git a/programs/programs/federal/pe/spm.py b/programs/programs/federal/pe/spm.py index df6f649c..74393ead 100644 --- a/programs/programs/federal/pe/spm.py +++ b/programs/programs/federal/pe/spm.py @@ -21,8 +21,14 @@ class Snap(PolicyEngineSpmCalulator): dependency.spm.SnapDependentCareDeductionDependency, dependency.spm.WaterExpenseDependency, dependency.spm.PhoneExpenseDependency, - # WARN: if you remove check that SNAP is still showing up - dependency.spm.TakesUpSnapIfEligibleDependency, + dependency.spm.HoaFeesExpenseDependency, + dependency.spm.HomeownersInsuranceExpenseDependency, + dependency.member.PropertyTaxExpenseDependency, + dependency.member.AgeDependency, + dependency.member.MedicalExpenseDependency, + dependency.member.IsDisabledDependency, + # NOTE: remove this to always use the SUA in CO. + dependency.spm.SnapAlwaysUseSuaDependency, ] pe_outputs = [dependency.spm.Snap] pe_period_month = "01" diff --git a/programs/programs/policyengine/calculators/dependencies/member.py b/programs/programs/policyengine/calculators/dependencies/member.py index 6c061f40..55713027 100644 --- a/programs/programs/policyengine/calculators/dependencies/member.py +++ b/programs/programs/policyengine/calculators/dependencies/member.py @@ -100,6 +100,34 @@ def value(self): return self.member.disabled or self.member.long_term_disability +# The Member class runs once per each household member, to ensure that the medical expenses +# are only counted once and only if a member is elderly or disabled; the medical expense is divided +# by the total number of elderly or disabled members. +class MedicalExpenseDependency(Member): + field = "medical_out_of_pocket_expenses" + + def value(self): + elderly_or_disabled_members = [ + member for member in self.screen.household_members.all() if member.age >= 60 or member.has_disability() + ] + count_of_elderly_or_disabled_members = len(elderly_or_disabled_members) + + if self.member.age >= 60 or self.member.has_disability(): + return self.screen.calc_expenses("yearly", ["medical"]) / count_of_elderly_or_disabled_members + + return 0 + + +class PropertyTaxExpenseDependency(Member): + field = "real_estate_taxes" + + def value(self): + if self.member.age >= 18: + return self.screen.calc_expenses("yearly", ["propertyTax"]) / self.screen.num_adults(18) + + return 0 + + class IsBlindDependency(Member): field = "is_blind" diff --git a/programs/programs/policyengine/calculators/dependencies/spm.py b/programs/programs/policyengine/calculators/dependencies/spm.py index fddc01d4..fe37707d 100644 --- a/programs/programs/policyengine/calculators/dependencies/spm.py +++ b/programs/programs/policyengine/calculators/dependencies/spm.py @@ -70,6 +70,13 @@ def value(self): return snap_gross_income < snap_gross_limit +class SnapAlwaysUseSuaDependency(SpmUnit): + field = "snap_state_using_standard_utility_allowance" + + def value(self): + return False + + class TakesUpSnapIfEligibleDependency(SpmUnit): field = "takes_up_snap_if_eligible" @@ -140,6 +147,20 @@ def value(self): return self.screen.calc_expenses("yearly", ["otherUtilities"]) +class HoaFeesExpenseDependency(SpmUnit): + field = "homeowners_association_fees" + + def value(self): + return self.screen.calc_expenses("yearly", ["hoa"]) + + +class HomeownersInsuranceExpenseDependency(SpmUnit): + field = "homeowners_insurance" + + def value(self): + return self.screen.calc_expenses("yearly", ["homeownersInsurance"]) + + class SnapEmergencyAllotmentDependency(SpmUnit): field = "snap_emergency_allotment" diff --git a/programs/programs/policyengine/policy_engine.py b/programs/programs/policyengine/policy_engine.py index bd9f95b3..9dc6918d 100644 --- a/programs/programs/policyengine/policy_engine.py +++ b/programs/programs/policyengine/policy_engine.py @@ -29,7 +29,6 @@ def calc_pe_eligibility( try: return all_eligibility(Method(input_data), valid_programs) except Exception as e: - print(e) capture_exception(e, level="warning", message="") capture_message(f"Failed to calculate eligibility with the {Method.method_name} method", level="warning") diff --git a/screener/migrations/0085_householdmember_birth_year_month_and_more.py b/screener/migrations/0085_householdmember_birth_year_month_and_more.py new file mode 100644 index 00000000..8c4fbfe0 --- /dev/null +++ b/screener/migrations/0085_householdmember_birth_year_month_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.15 on 2024-10-01 22:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("screener", "0084_screen_has_fatc"), + ] + + operations = [ + migrations.AddField( + model_name="householdmember", + name="birth_year_month", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterField( + model_name="householdmember", + name="age", + field=models.PositiveIntegerField(blank=True, null=True), + ), + ] diff --git a/screener/models.py b/screener/models.py index 86351b33..ae27e042 100644 --- a/screener/models.py +++ b/screener/models.py @@ -1,3 +1,5 @@ +from datetime import datetime +from typing import Optional from django.db import models from decimal import Decimal import uuid @@ -380,7 +382,8 @@ class Message(models.Model): class HouseholdMember(models.Model): screen = models.ForeignKey(Screen, related_name="household_members", on_delete=models.CASCADE) relationship = models.CharField(max_length=30, blank=True, null=True) - age = models.IntegerField(blank=True, null=True) + age = models.PositiveIntegerField(blank=True, null=True) + birth_year_month = models.DateField(blank=True, null=True) student = models.BooleanField(blank=True, null=True) student_full_time = models.BooleanField(blank=True, null=True) pregnant = models.BooleanField(blank=True, null=True) @@ -466,6 +469,35 @@ def is_dependent(self): def is_in_tax_unit(self): return self.is_head() or self.is_spouse() or self.is_dependent() + @property + def birth_year(self) -> Optional[int]: + if self.birth_year_month is None: + return None + + return self.birth_year_month.year + + @property + def birth_month(self) -> Optional[int]: + if self.birth_year_month is None: + return None + + return self.birth_year_month.month + + def calc_age(self) -> int: + if self.birth_year_month is None: + return self.age + + return self.age_from_date(self.birth_year_month) + + @staticmethod + def age_from_date(birth_year_month: datetime) -> int: + today = datetime.now() + + if today.month >= birth_year_month.month: + return today.year - birth_year_month.year + + return today.year - birth_year_month.year - 1 + def missing_fields(self): member_fields = ( "relationship", diff --git a/screener/serializers.py b/screener/serializers.py index 61542dc6..1ccb9bee 100644 --- a/screener/serializers.py +++ b/screener/serializers.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta, date from screener.models import Screen, HouseholdMember, IncomeStream, Expense, Message, Insurance from authentication.serializers import UserOffersSerializer from rest_framework import serializers @@ -43,6 +44,33 @@ class Meta: class HouseholdMemberSerializer(serializers.ModelSerializer): income_streams = IncomeStreamSerializer(many=True) insurance = InsuranceSerializer() + birth_year = serializers.IntegerField(required=False, allow_null=True) + birth_month = serializers.IntegerField(required=False, allow_null=True) + + def validate(self, data): + birth_year = data.pop("birth_year", None) + birth_month = data.pop("birth_month", None) + + if birth_year is None or birth_month is None: + return data + + if birth_month < 1 or birth_month > 12: + raise serializers.ValidationError("Birth month must be between 1 and 12") + + birth_year_month = datetime(year=birth_year, month=birth_month, day=1) + + # add a day for timezones + today = datetime.now() + timedelta(days=1) + + if birth_year_month > today: + raise serializers.ValidationError("Birth year and month are in the future") + + data["birth_year_month"] = birth_year_month.date() + + if "age" not in data or data["age"] is None: + data["age"] = HouseholdMember.age_from_date(birth_year_month) + + return data class Meta: model = HouseholdMember @@ -65,6 +93,8 @@ class Meta: "has_income", "income_streams", "insurance", + "birth_year", + "birth_month", ) read_only_fields = ("screen", "id") diff --git a/screener/views.py b/screener/views.py index 0135ac7b..822e0c66 100644 --- a/screener/views.py +++ b/screener/views.py @@ -73,7 +73,7 @@ def update(self, request, pk=None): user = get_object_or_404(queryset, uuid=pk) body = json.loads(request.body.decode()) serializer = ScreenSerializer(user, data=body) - serializer.is_valid() + serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data)