Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TCCP: Display account fee as a number #8281

Merged
merged 3 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cfgov/tccp/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ class CurrencyField(models.TextField):
]


class CurrencyDecimalField(models.DecimalField):
default_validators = models.DecimalField.default_validators + [
RegexValidator(CURRENCY_REGEX)
]

def __init__(self, *args, **kwargs):
kwargs.setdefault("decimal_places", 2)
kwargs.setdefault("max_digits", 10)
super().__init__(*args, **kwargs)

def to_python(self, value):
if isinstance(value, str):
value = value.lstrip("$")

return super().to_python(value)


class JSONListField(models.JSONField):
default_error_messages = {"invalid_value": "%(value)s must be a list."}
description = "A JSON list"
Expand Down
11 changes: 9 additions & 2 deletions cfgov/tccp/jinja2/tccp/includes/card_list.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% from 'tccp/includes/data_published.html' import data_published %}
{% from 'tccp/includes/fields.html' import apr, apr_range %}
{% from 'tccp/includes/fields.html' import apr, apr_range, currency %}
{% import 'v1/includes/molecules/breadcrumbs.html' as breadcrumbs with context %}
{% import 'v1/includes/molecules/notification.html' as notification %}

Expand Down Expand Up @@ -81,7 +81,14 @@
{% do card_rows.append( [
card_name_cell(card) | safe,
purchase_apr_cell(card) | safe,
(card.periodic_fee_type | join(', ')) if card.periodic_fee_type else 'None',
currency(
card.annual_fee_estimated,
default=(
'See details'
if card.annual_fee_estimated is none and card.periodic_fee_type
else '$0'
)
),
apr(card.transfer_apr_for_tier) if card.transfer_apr_for_tier is not none else apr_range(card.transfer_apr_min, card.transfer_apr_max),
(rewards | join(', ')) if rewards else 'None'
] ) %}
Expand Down
8 changes: 8 additions & 0 deletions cfgov/tccp/jinja2/tccp/includes/fields.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@
{{ apr(min) }} - {{ apr(max) }}
{%- endif %}
{%- endmacro -%}

{%- macro currency(value, default=none, boolean=false) -%}
{{
('$' ~ ('%.0f' if value == (value | int) else '%.2f') | format(value))
if value is not none and (value or not boolean)
else (default if default is not none else "None")
}}
{%- endmacro -%}
149 changes: 149 additions & 0 deletions cfgov/tccp/migrations/0010_currencydecimalfield.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Generated by Django 3.2.24 on 2024-04-01 17:18

from django.db import migrations
import tccp.fields


class Migration(migrations.Migration):

dependencies = [
('tccp', '0009_alter_cardsurveydata_rewards'),
]

operations = [
migrations.AlterField(
model_name='cardsurveydata',
name='annual_fee',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='balance_transfer_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='cash_advance_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='foreign_transaction_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='late_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='late_fee_six_month_billing_cycle',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='maximum38',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='maximum58',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum37',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum57',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum_balance_transfer_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum_cash_advance_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum_finance_charge_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum_foreign_transaction_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='minimum_purchase_transaction_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='monthly_fee',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_fee_amount_2',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_fee_amount_3',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_fee_amount_4',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_fee_amount_5',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='other_periodic_fee_amount',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='over_limit_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='periodic_max',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='periodic_min',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='purchase_transaction_fee_dollars',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
migrations.AlterField(
model_name='cardsurveydata',
name='weekly_fee',
field=tccp.fields.CurrencyDecimalField(blank=True, decimal_places=2, max_digits=10, null=True),
),
]
89 changes: 61 additions & 28 deletions cfgov/tccp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from tailslide import Percentile

from . import enums
from .fields import CurrencyField, JSONListField, YesNoBooleanField
from .fields import CurrencyDecimalField, JSONListField, YesNoBooleanField


REPORT_DATE_REGEX = re.compile(r"Data as of (\w+ \d+)")
Expand Down Expand Up @@ -251,7 +251,9 @@ class CardSurveyData(models.Model):
grace_period_offered = YesNoBooleanField()
grace_period = models.PositiveIntegerField(null=True, blank=True)
minimum_finance_charge = YesNoBooleanField()
minimum_finance_charge_dollars = CurrencyField(null=True, blank=True)
minimum_finance_charge_dollars = CurrencyDecimalField(
null=True, blank=True
)
balance_computation_method = JSONListField(
choices=enums.BalanceComputationChoices
)
Expand All @@ -261,25 +263,27 @@ class CardSurveyData(models.Model):
periodic_fee_type = JSONListField(
choices=enums.PeriodicFeeTypeChoices, blank=True
)
annual_fee = CurrencyField(null=True, blank=True)
monthly_fee = CurrencyField(null=True, blank=True)
weekly_fee = CurrencyField(null=True, blank=True)
annual_fee = CurrencyDecimalField(null=True, blank=True)
monthly_fee = CurrencyDecimalField(null=True, blank=True)
weekly_fee = CurrencyDecimalField(null=True, blank=True)
other_periodic_fee_name = models.TextField(null=True, blank=True)
other_periodic_fee_amount = CurrencyField(null=True, blank=True)
other_periodic_fee_amount = CurrencyDecimalField(null=True, blank=True)
other_periodic_fee_frequency = models.TextField(null=True, blank=True)
fee_varies = YesNoBooleanField(null=True, blank=True)
periodic_min = CurrencyField(null=True, blank=True)
periodic_max = CurrencyField(null=True, blank=True)
periodic_min = CurrencyDecimalField(null=True, blank=True)
periodic_max = CurrencyDecimalField(null=True, blank=True)
fee_explanation = models.TextField(null=True, blank=True)
purchase_transaction_fees = YesNoBooleanField()
purchase_transaction_fee_type = JSONListField(
choices=enums.PurchaseTransactionFeeTypeChoices, blank=True
)
purchase_transaction_fee_dollars = CurrencyField(null=True, blank=True)
purchase_transaction_fee_dollars = CurrencyDecimalField(
null=True, blank=True
)
purchase_transaction_fee_percentage = models.FloatField(
null=True, blank=True
)
minimum_purchase_transaction_fee_amount = CurrencyField(
minimum_purchase_transaction_fee_amount = CurrencyDecimalField(
null=True, blank=True
)
purchase_transaction_fee_calculation = models.TextField(
Expand All @@ -289,9 +293,11 @@ class CardSurveyData(models.Model):
balance_transfer_fee_types = JSONListField(
choices=enums.BalanceTransferFeeTypeChoices, blank=True
)
balance_transfer_fee_dollars = CurrencyField(null=True, blank=True)
balance_transfer_fee_dollars = CurrencyDecimalField(null=True, blank=True)
balance_transfer_fee_percentage = models.FloatField(null=True, blank=True)
minimum_balance_transfer_fee_amount = CurrencyField(null=True, blank=True)
minimum_balance_transfer_fee_amount = CurrencyDecimalField(
null=True, blank=True
)
balance_transfer_fee_calculation = models.TextField(null=True, blank=True)
cash_advance_fees = YesNoBooleanField()
cash_advance_fee_for_each_transaction = YesNoBooleanField(
Expand All @@ -300,19 +306,23 @@ class CardSurveyData(models.Model):
cash_advance_fee_types = JSONListField(
choices=enums.CashAdvanceFeeTypeChoices, blank=True
)
cash_advance_fee_dollars = CurrencyField(null=True, blank=True)
cash_advance_fee_dollars = CurrencyDecimalField(null=True, blank=True)
cash_advance_fee_percentage = models.FloatField(null=True, blank=True)
minimum_cash_advance_fee_amount = CurrencyField(null=True, blank=True)
minimum_cash_advance_fee_amount = CurrencyDecimalField(
null=True, blank=True
)
cash_advance_fee_calculation = models.TextField(null=True, blank=True)
foreign_transaction_fees = YesNoBooleanField()
foreign_transaction_fees_types = JSONListField(
choices=enums.ForeignTransactionFeeTypeChoices, blank=True
)
foreign_transaction_fee_dollars = CurrencyField(null=True, blank=True)
foreign_transaction_fee_dollars = CurrencyDecimalField(
null=True, blank=True
)
foreign_transaction_fee_percentage = models.FloatField(
null=True, blank=True
)
minimum_foreign_transaction_fee_amount = CurrencyField(
minimum_foreign_transaction_fee_amount = CurrencyDecimalField(
null=True, blank=True
)
foreign_transaction_fee_calculation = models.TextField(
Expand All @@ -322,39 +332,41 @@ class CardSurveyData(models.Model):
late_fee_types = JSONListField(
choices=enums.LateFeeTypeChoices, blank=True
)
late_fee_dollars = CurrencyField(null=True, blank=True)
late_fee_six_month_billing_cycle = CurrencyField(null=True, blank=True)
late_fee_dollars = CurrencyDecimalField(null=True, blank=True)
late_fee_six_month_billing_cycle = CurrencyDecimalField(
null=True, blank=True
)
late_fee_policy_details = models.TextField(null=True, blank=True)
fee_varies36 = YesNoBooleanField(null=True, blank=True)
minimum37 = CurrencyField(null=True, blank=True)
maximum38 = CurrencyField(null=True, blank=True)
minimum37 = CurrencyDecimalField(null=True, blank=True)
maximum38 = CurrencyDecimalField(null=True, blank=True)
fee_explanation39 = models.TextField(null=True, blank=True)
over_limit_fees = YesNoBooleanField()
over_limit_fee_types = JSONListField(
choices=enums.OverlimitFeeTypeChoices, blank=True
)
over_limit_fee_dollars = CurrencyField(null=True, blank=True)
over_limit_fee_dollars = CurrencyDecimalField(null=True, blank=True)
overlimit_fee_detail = models.TextField(null=True, blank=True)
other_fees = YesNoBooleanField()
additional_fees = YesNoBooleanField(null=True, blank=True)
other_fee_name = models.TextField(null=True, blank=True)
other_fee_amount = CurrencyField(null=True, blank=True)
other_fee_amount = CurrencyDecimalField(null=True, blank=True)
other_fee_explanation = models.TextField(null=True, blank=True)
other_fee_name_2 = models.TextField(null=True, blank=True)
other_fee_amount_2 = CurrencyField(null=True, blank=True)
other_fee_amount_2 = CurrencyDecimalField(null=True, blank=True)
other_fee_explanation_2 = models.TextField(null=True, blank=True)
other_fee_name_3 = models.TextField(null=True, blank=True)
other_fee_amount_3 = CurrencyField(null=True, blank=True)
other_fee_amount_3 = CurrencyDecimalField(null=True, blank=True)
other_fee_explanation_3 = models.TextField(null=True, blank=True)
other_fee_name_4 = models.TextField(null=True, blank=True)
other_fee_amount_4 = CurrencyField(null=True, blank=True)
other_fee_amount_4 = CurrencyDecimalField(null=True, blank=True)
other_fee_explanation_4 = models.TextField(null=True, blank=True)
other_fee_name_5 = models.TextField(null=True, blank=True)
other_fee_amount_5 = CurrencyField(null=True, blank=True)
other_fee_amount_5 = CurrencyDecimalField(null=True, blank=True)
other_fee_explanation_5 = models.TextField(null=True, blank=True)
fee_varies56 = YesNoBooleanField(null=True, blank=True)
minimum57 = CurrencyField(null=True, blank=True)
maximum58 = CurrencyField(null=True, blank=True)
minimum57 = CurrencyDecimalField(null=True, blank=True)
maximum58 = CurrencyDecimalField(null=True, blank=True)
fee_explanation59 = models.TextField(null=True, blank=True)
services = JSONListField(choices=enums.ServicesChoices, blank=True)
other_services = models.TextField(null=True, blank=True)
Expand Down Expand Up @@ -399,3 +411,24 @@ class Meta:
]

objects = CardSurveyDataQuerySet.as_manager()

@property
def annual_fee_estimated(self):
"""Estimate a card's annual fee from its periodic fees.

If a card has "Other" periodic fees, we can't accurately estimate its
annual fee.
"""
if "Other" in self.periodic_fee_type:
return None

fee = 0

if "Annual" in self.periodic_fee_type and self.annual_fee:
fee += self.annual_fee
if "Monthly" in self.periodic_fee_type and self.monthly_fee:
fee += 12 * self.monthly_fee
if "Weekly" in self.periodic_fee_type and self.weekly_fee:
fee += 52 * self.weekly_fee

return fee
Loading
Loading