diff --git a/django/api/admin.py b/django/api/admin.py index fcff99ee..40f138c5 100644 --- a/django/api/admin.py +++ b/django/api/admin.py @@ -2,6 +2,7 @@ from .models.go_electric_rebate_application import ( GoElectricRebateApplication, SubmittedGoElectricRebateApplication, + InitiatedGoElectricRebateApplication, ) from .models.household_member import HouseholdMember from .models.go_electric_rebate import GoElectricRebate @@ -42,6 +43,7 @@ class GoElectricRebateApplicationAdmin(admin.ModelAdmin): # by BCeID users. @admin.register(SubmittedGoElectricRebateApplication) class SubmittedGoElectricRebateApplicationAdmin(admin.ModelAdmin): + search_fields = ["drivers_licence", "id", "status"] # disable bulk actions actions = None exclude = ( @@ -70,6 +72,7 @@ class SubmittedGoElectricRebateApplicationAdmin(admin.ModelAdmin): "doc2_tag", "consent_personal", "consent_tax", + "is_legacy", ) def get_queryset(self, request): @@ -114,3 +117,66 @@ def message_user( @admin.register(GoElectricRebate) class GoElectricRebateAdmin(admin.ModelAdmin): pass + + +@admin.register(InitiatedGoElectricRebateApplication) +class InitiatedGoElectricRebateApplicationAdmin(admin.ModelAdmin): + search_fields = ["drivers_licence", "id", "status"] + # disable bulk actions + actions = None + exclude = ( + "sin", + "doc1", + "doc2", + "user", + "spouse_email", + "status", + "address", + "city", + "postal_code", + "application_type", + "doc1_tag", + "doc2_tag", + "consent_personal", + "consent_tax", + ) + readonly_fields = ( + "id", + "last_name", + "first_name", + "middle_names", + "email", + "user_is_bcsc", + "drivers_licence", + "date_of_birth", + "tax_year", + "is_legacy", + ) + + def get_queryset(self, request): + return GoElectricRebateApplication.objects.filter( + status=GoElectricRebateApplication.Status.HOUSEHOLD_INITIATED + ) + + def has_delete_permission(self, request, obj=None): + return False + + def response_change(self, request, obj): + ret = super().response_change(request, obj) + if "cancel_application" in request.POST: + obj.status = GoElectricRebateApplication.Status.CANCELLED + obj.save(update_fields=["status"]) + return ret + + def message_user( + self, + request, + message, + level=messages.INFO, + extra_tags="", + fail_silently=False, + ): + revised_level = level + if "cancel_application" in request.POST: + revised_level = messages_custom.NEGATIVE_SUCCESS + super().message_user(request, message, revised_level, extra_tags, fail_silently) diff --git a/django/api/apps.py b/django/api/apps.py index 7ef94cfc..bf9b64ef 100644 --- a/django/api/apps.py +++ b/django/api/apps.py @@ -1,5 +1,7 @@ from django.apps import AppConfig from django.contrib.admin.apps import AdminConfig +from . import settings +import sys class ApiConfig(AppConfig): @@ -7,7 +9,11 @@ class ApiConfig(AppConfig): verbose_name = "Submitted Rebate Applications" def ready(self): - import api.signals + import api.signal_receivers + from api.scheduled_jobs import schedule_get_ncda_redeemed_rebates + + if settings.RUN_JOBS and "qcluster" in sys.argv: + schedule_get_ncda_redeemed_rebates() class ITVRAdminConfig(AdminConfig): diff --git a/django/api/authentication/keycloak.py b/django/api/authentication/keycloak.py index a9b4c5d0..51f99e6c 100644 --- a/django/api/authentication/keycloak.py +++ b/django/api/authentication/keycloak.py @@ -69,9 +69,9 @@ def authenticate_credentials(self, token): user, created = ITVRUser.objects.get_or_create( username=token_info.get("sub"), defaults={ - "display_name": token_info.get("display_name"), + "display_name": token_info.get("display_name", ""), "email": token_info.get("email", ""), - "identity_provider": token_info.get("identity_provider"), + "identity_provider": token_info.get("identity_provider", ""), }, ) diff --git a/django/api/migrations/0003_goelectricrebateapplication_is_legacy_and_more.py b/django/api/migrations/0003_goelectricrebateapplication_is_legacy_and_more.py new file mode 100644 index 00000000..6d457bcd --- /dev/null +++ b/django/api/migrations/0003_goelectricrebateapplication_is_legacy_and_more.py @@ -0,0 +1,127 @@ +# Generated by Django 4.0.1 on 2022-06-30 15:56 + +import api.validators +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import encrypted_fields.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('api', '0002_alter_goelectricrebate_ncda_id'), + ] + + operations = [ + migrations.AddField( + model_name='goelectricrebateapplication', + name='is_legacy', + field=models.BooleanField(default=False, editable=False), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='address', + field=models.CharField(max_length=250, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='application_type', + field=models.CharField(max_length=25, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='city', + field=models.CharField(max_length=250, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='consent_personal', + field=models.BooleanField(null=True, validators=[api.validators.validate_consent]), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='consent_tax', + field=models.BooleanField(null=True, validators=[api.validators.validate_consent]), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='date_of_birth', + field=models.DateField(null=True, validators=[api.validators.validate_driving_age]), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='email', + field=models.EmailField(max_length=250, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='first_name', + field=models.CharField(max_length=250, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='last_name', + field=models.CharField(max_length=250, null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='sin', + field=encrypted_fields.fields.EncryptedCharField(max_length=9, null=True, validators=[api.validators.validate_sin]), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='tax_year', + field=models.IntegerField(null=True), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='user', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('user__isnull', False), _connector='OR'), name='user_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('sin__isnull', False), _connector='OR'), name='sin_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('last_name__isnull', False), _connector='OR'), name='last_name_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('first_name__isnull', False), _connector='OR'), name='first_name_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('email__isnull', False), _connector='OR'), name='email_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('address__isnull', False), _connector='OR'), name='address_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('city__isnull', False), _connector='OR'), name='city_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('date_of_birth__isnull', False), _connector='OR'), name='date_of_birth_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('tax_year__isnull', False), _connector='OR'), name='tax_year_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('consent_personal__isnull', False), _connector='OR'), name='consent_personal_null_constraint'), + ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('consent_tax__isnull', False), _connector='OR'), name='consent_tax_null_constraint'), + ), + ] diff --git a/django/api/migrations/0004_alter_goelectricrebateapplication_status.py b/django/api/migrations/0004_alter_goelectricrebateapplication_status.py new file mode 100644 index 00000000..a444000b --- /dev/null +++ b/django/api/migrations/0004_alter_goelectricrebateapplication_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.1 on 2022-07-06 20:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_goelectricrebateapplication_is_legacy_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='goelectricrebateapplication', + name='status', + field=models.CharField(choices=[('household_initiated', 'Household Initiated'), ('submitted', 'Submitted'), ('verified', 'Verified'), ('declined', 'Declined'), ('approved', 'Approved'), ('not_approved', 'Not Approved'), ('redeemed', 'Redeemed'), ('expired', 'Expired'), ('cancelled', 'Cancelled')], max_length=250), + ), + ] diff --git a/django/api/migrations/0005_goelectricrebateapplication_application_type_null_constraint.py b/django/api/migrations/0005_goelectricrebateapplication_application_type_null_constraint.py new file mode 100644 index 00000000..fd7cded8 --- /dev/null +++ b/django/api/migrations/0005_goelectricrebateapplication_application_type_null_constraint.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.1 on 2022-07-11 18:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_alter_goelectricrebateapplication_status'), + ] + + operations = [ + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.CheckConstraint(check=models.Q(('is_legacy__exact', True), ('application_type__isnull', False), _connector='OR'), name='application_type_null_constraint'), + ), + ] diff --git a/django/api/migrations/0006_initiatedgoelectricrebateapplication.py b/django/api/migrations/0006_initiatedgoelectricrebateapplication.py new file mode 100644 index 00000000..36111cc9 --- /dev/null +++ b/django/api/migrations/0006_initiatedgoelectricrebateapplication.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.1 on 2022-07-12 17:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_goelectricrebateapplication_application_type_null_constraint'), + ] + + operations = [ + migrations.CreateModel( + name='InitiatedGoElectricRebateApplication', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('api.goelectricrebateapplication',), + ), + ] diff --git a/django/api/migrations/0007_alter_goelectricrebate_options_and_more.py b/django/api/migrations/0007_alter_goelectricrebate_options_and_more.py new file mode 100644 index 00000000..d4b1be25 --- /dev/null +++ b/django/api/migrations/0007_alter_goelectricrebate_options_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.0.1 on 2022-07-12 21:56 + +import api.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0006_initiatedgoelectricrebateapplication'), + ] + + operations = [ + migrations.AlterModelOptions( + name='goelectricrebate', + options={}, + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='doc1', + field=models.ImageField(blank=True, null=True, upload_to='docs', validators=[api.validators.validate_file_size]), + ), + migrations.AlterField( + model_name='goelectricrebateapplication', + name='doc2', + field=models.ImageField(blank=True, null=True, upload_to='docs', validators=[api.validators.validate_file_size]), + ), + migrations.AlterField( + model_name='householdmember', + name='doc1', + field=models.ImageField(blank=True, null=True, upload_to='docs', validators=[api.validators.validate_file_size]), + ), + migrations.AlterField( + model_name='householdmember', + name='doc2', + field=models.ImageField(blank=True, null=True, upload_to='docs', validators=[api.validators.validate_file_size]), + ), + migrations.AlterModelTable( + name='goelectricrebate', + table='go_electric_rebate', + ), + ] diff --git a/django/api/models/go_electric_rebate.py b/django/api/models/go_electric_rebate.py index 5e3b8634..3c46fb88 100644 --- a/django/api/models/go_electric_rebate.py +++ b/django/api/models/go_electric_rebate.py @@ -5,7 +5,6 @@ BooleanField, PROTECT, ForeignKey, - UUIDField, ) from django.core.validators import MinLengthValidator @@ -31,3 +30,6 @@ class GoElectricRebate(TimeStampedModel): def __str__(self): return "DL: " + self.drivers_licence + ", $" + str(self.rebate_max_amount) + + class Meta: + db_table = "go_electric_rebate" diff --git a/django/api/models/go_electric_rebate_application.py b/django/api/models/go_electric_rebate_application.py index de8c0aa8..b1f38032 100644 --- a/django/api/models/go_electric_rebate_application.py +++ b/django/api/models/go_electric_rebate_application.py @@ -13,6 +13,7 @@ Manager, Q, UniqueConstraint, + CheckConstraint, ) from encrypted_fields.fields import EncryptedCharField from django.utils.html import mark_safe @@ -22,10 +23,12 @@ validate_driving_age, validate_sin, validate_consent, + validate_file_size, ) from django_extensions.db.models import TimeStampedModel from django.utils.translation import gettext_lazy as _ from django.utils.functional import classproperty +from api.signals import household_application_saved media_storage = get_storage_class()() @@ -33,11 +36,14 @@ class ApplicationManager(Manager): def create(self, **kwargs): spouse_email = kwargs.pop("spouse_email", None) - obj = self.model(**kwargs) - self._for_write = True + obj = super().create(**kwargs) if spouse_email: - obj.spouse_email = spouse_email - obj.save(force_insert=True, using=self.db) + household_application_saved.send( + sender=GoElectricRebateApplication, + instance=obj, + created=True, + spouse_email=spouse_email, + ) return obj @@ -53,27 +59,30 @@ class Status(TextChoices): NOT_APPROVED = ("not_approved", _("Not Approved")) REDEEMED = ("redeemed", _("Redeemed")) EXPIRED = ("expired", _("Expired")) + CANCELLED = ("cancelled", _("Cancelled")) - user = ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=PROTECT, - ) + user = ForeignKey(settings.AUTH_USER_MODEL, on_delete=PROTECT, null=True) id = ShortUUIDField(length=16, primary_key=True, editable=False) - sin = EncryptedCharField(max_length=9, unique=False, validators=[validate_sin]) + is_legacy = BooleanField(editable=False, default=False) + sin = EncryptedCharField( + max_length=9, unique=False, validators=[validate_sin], null=True + ) status = CharField(max_length=250, choices=Status.choices, unique=False) - last_name = CharField(max_length=250, unique=False) - first_name = CharField(max_length=250, unique=False) + last_name = CharField(max_length=250, unique=False, null=True) + first_name = CharField(max_length=250, unique=False, null=True) middle_names = CharField(max_length=250, unique=False, blank=True, null=True) - email = EmailField(max_length=250, unique=False) - address = CharField(max_length=250, unique=False) - city = CharField(max_length=250, unique=False) + email = EmailField(max_length=250, unique=False, null=True) + address = CharField(max_length=250, unique=False, null=True) + city = CharField(max_length=250, unique=False, null=True) postal_code = CharField(max_length=6, unique=False, blank=True, null=True) drivers_licence = CharField( max_length=8, unique=False, validators=[MinLengthValidator(7)] ) - date_of_birth = DateField(validators=[validate_driving_age]) - tax_year = IntegerField() - doc1 = ImageField(upload_to="docs", blank=True, null=True) + date_of_birth = DateField(validators=[validate_driving_age], null=True) + tax_year = IntegerField(null=True) + doc1 = ImageField( + upload_to="docs", blank=True, null=True, validators=[validate_file_size] + ) def doc1_tag(self): return mark_safe( @@ -83,7 +92,9 @@ def doc1_tag(self): doc1_tag.short_description = "First Uploaded Document" - doc2 = ImageField(upload_to="docs", blank=True, null=True) + doc2 = ImageField( + upload_to="docs", blank=True, null=True, validators=[validate_file_size] + ) def doc2_tag(self): return mark_safe( @@ -97,9 +108,10 @@ def doc2_tag(self): application_type = CharField( max_length=25, unique=False, + null=True, ) - consent_personal = BooleanField(validators=[validate_consent]) - consent_tax = BooleanField(validators=[validate_consent]) + consent_personal = BooleanField(validators=[validate_consent], null=True) + consent_tax = BooleanField(validators=[validate_consent], null=True) def user_is_bcsc(self): if self.user.identity_provider == "bcsc": @@ -109,7 +121,18 @@ def user_is_bcsc(self): user_is_bcsc.short_description = "Address is BCSC Verified" def __str__(self): - return self.last_name + ", " + self.first_name + ": " + str(self.id) + if self.is_legacy: + return "preITVR " + str(self.id) + else: + return ( + self.last_name + + ", " + + self.first_name + + ": " + + str(self.id) + + ": " + + self.status + ) class Meta: db_table = "go_electric_rebate_application" @@ -126,7 +149,55 @@ class Meta: ] ), name="verify_rebate_status", - ) + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(user__isnull=False), + name="user_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(sin__isnull=False), + name="sin_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(last_name__isnull=False), + name="last_name_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(first_name__isnull=False), + name="first_name_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(email__isnull=False), + name="email_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(address__isnull=False), + name="address_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(city__isnull=False), + name="city_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(date_of_birth__isnull=False), + name="date_of_birth_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(tax_year__isnull=False), + name="tax_year_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(application_type__isnull=False), + name="application_type_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(consent_personal__isnull=False), + name="consent_personal_null_constraint", + ), + CheckConstraint( + check=Q(is_legacy__exact=True) | Q(consent_tax__isnull=False), + name="consent_tax_null_constraint", + ), ] @@ -142,3 +213,16 @@ def admin_label(cls): @classproperty def admin_display_change(cls): return False + + +class InitiatedGoElectricRebateApplication(GoElectricRebateApplication): + class Meta: + proxy = True + + @classproperty + def admin_label(cls): + return "Cancel Applications" + + @classproperty + def admin_display_change(cls): + return False diff --git a/django/api/models/household_member.py b/django/api/models/household_member.py index fd015038..33949c4e 100644 --- a/django/api/models/household_member.py +++ b/django/api/models/household_member.py @@ -16,6 +16,7 @@ validate_driving_age, validate_sin, validate_consent, + validate_file_size, ) media_storage = get_storage_class()() @@ -38,7 +39,9 @@ class HouseholdMember(TimeStampedModel): bcsc_address = CharField(max_length=250, unique=False, blank=True, null=True) bcsc_city = CharField(max_length=250, unique=False, blank=True, null=True) bcsc_postal_code = CharField(max_length=6, unique=False, blank=True, null=True) - doc1 = ImageField(upload_to="docs", blank=True, null=True) + doc1 = ImageField( + upload_to="docs", blank=True, null=True, validators=[validate_file_size] + ) def doc1_tag(self): return mark_safe( @@ -48,7 +51,9 @@ def doc1_tag(self): doc1_tag.short_description = "First Uploaded Document" - doc2 = ImageField(upload_to="docs", blank=True, null=True) + doc2 = ImageField( + upload_to="docs", blank=True, null=True, validators=[validate_file_size] + ) def doc2_tag(self): return mark_safe( diff --git a/django/api/scheduled_jobs.py b/django/api/scheduled_jobs.py new file mode 100644 index 00000000..b910b79b --- /dev/null +++ b/django/api/scheduled_jobs.py @@ -0,0 +1,12 @@ +from django_q.tasks import schedule +from django_q.models import Schedule + + +def schedule_exists(func_name): + return Schedule.objects.filter(func__exact=func_name).exists() + + +def schedule_get_ncda_redeemed_rebates(): + task_name = "api.tasks.check_rebates_redeemed_since" + if not schedule_exists(task_name): + schedule(task_name, None, task_name, schedule_type="D") diff --git a/django/api/serializers/application_form.py b/django/api/serializers/application_form.py index 6649575d..8fdc1d27 100644 --- a/django/api/serializers/application_form.py +++ b/django/api/serializers/application_form.py @@ -1,12 +1,11 @@ -from api.validators import validate_file_size from rest_framework.serializers import ModelSerializer, SerializerMethodField from api.models.go_electric_rebate_application import GoElectricRebateApplication from rest_framework.parsers import FormParser, MultiPartParser from datetime import date -from django.core.exceptions import ValidationError from rest_framework.response import Response from rest_framework import status + class ApplicationFormCreateSerializer(ModelSerializer): parser_classes = ( MultiPartParser, @@ -15,7 +14,7 @@ class ApplicationFormCreateSerializer(ModelSerializer): class Meta: model = GoElectricRebateApplication - exclude = ["user", "status", "tax_year"] + exclude = ["user", "status", "tax_year", "is_legacy"] def _get_tax_year(self): today = date.today() @@ -27,13 +26,6 @@ def _get_tax_year(self): class ApplicationFormCreateSerializerDefault(ApplicationFormCreateSerializer): - def validate(self, data): - if data.get("doc1") is None or data.get("doc2") is None: - raise ValidationError("Missing required document.") - validate_file_size(data["doc1"]) - validate_file_size(data["doc2"]) - return data - def create(self, validated_data): request = self.context["request"] user = request.user @@ -152,8 +144,4 @@ class Meta: class ApplicationFormSpouseSerializer(ModelSerializer): class Meta: model = GoElectricRebateApplication - fields = [ - "address", - "city", - "postal_code", - ] + fields = ["address", "city", "postal_code", "status"] diff --git a/django/api/serializers/household_member.py b/django/api/serializers/household_member.py index 06f15ca8..c92908df 100644 --- a/django/api/serializers/household_member.py +++ b/django/api/serializers/household_member.py @@ -1,8 +1,6 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField from api.models.household_member import HouseholdMember from rest_framework.parsers import FormParser, MultiPartParser -from api.validators import validate_file_size -from django.core.exceptions import ValidationError class HouseholdMemberApplicationCreateSerializer(ModelSerializer): @@ -19,13 +17,6 @@ class Meta: class HouseholdMemberApplicationCreateSerializerDefault( HouseholdMemberApplicationCreateSerializer ): - def validate(self, data): - if data.get("doc1") is None or data.get("doc2") is None: - raise ValidationError("Missing required document.") - validate_file_size(data["doc1"]) - validate_file_size(data["doc2"]) - return data - def create(self, validated_data): user = self.context["request"].user diff --git a/django/api/settings.py b/django/api/settings.py index 72495d6c..f3e3743b 100644 --- a/django/api/settings.py +++ b/django/api/settings.py @@ -251,3 +251,5 @@ ) MESSAGE_TAGS = messages_custom.TAGS + +RUN_JOBS = os.getenv("RUN_JOBS", False) diff --git a/django/api/signal_receivers.py b/django/api/signal_receivers.py new file mode 100644 index 00000000..7b1ec91e --- /dev/null +++ b/django/api/signal_receivers.py @@ -0,0 +1,104 @@ +from django.db.models.signals import post_save +from .models.go_electric_rebate_application import ( + GoElectricRebateApplication, +) +from .models.household_member import HouseholdMember +from .models.go_electric_rebate import GoElectricRebate +from django.dispatch import receiver +from django.conf import settings +from api.models.household_member import HouseholdMember +from django_q.tasks import async_task +from api.utility import addresses_match +from .signals import household_application_saved + + +@receiver(post_save, sender=GoElectricRebateApplication) +def create_application(sender, instance, created, **kwargs): + if created and settings.EMAIL["SEND_EMAIL"]: + async_task("api.tasks.send_individual_confirm", instance.email, instance.id) + + +@receiver(household_application_saved, sender=GoElectricRebateApplication) +def after_household_application_created(sender, instance, created, **kwargs): + spouse_email = kwargs.get("spouse_email") + async_task( + "api.tasks.send_spouse_initial_message", + spouse_email, + instance.id, + instance.email, + ) + + +@receiver(post_save, sender=HouseholdMember) +def after_household_member_save(sender, instance, created, **kwargs): + if created: + application = instance.application + primary_user = application.user + secondary_user = instance.user + if application.status != GoElectricRebateApplication.Status.CANCELLED: + if ( + primary_user.identity_provider == "bcsc" + and secondary_user.identity_provider == "bcsc" + and addresses_match(application, secondary_user) + ): + application.status = GoElectricRebateApplication.Status.VERIFIED + application.save() + else: + application.status = GoElectricRebateApplication.Status.SUBMITTED + application.save() + + if settings.EMAIL["SEND_EMAIL"]: + async_task( + "api.tasks.send_household_confirm", + application.email, + application.id, + ) + + +@receiver(post_save, sender=GoElectricRebateApplication) +def after_status_change(sender, instance, created, **kwargs): + if ( + (not created) + and (kwargs.get("update_fields") == {"status"}) + and (settings.EMAIL["SEND_EMAIL"]) + ): + # if identity is declined (eg id doesnt match address) + if instance.status == GoElectricRebateApplication.Status.DECLINED: + async_task( + "api.tasks.send_reject", + instance.email, + instance.id, + ) + # if rebate is approved, send an email with amount + elif instance.status == GoElectricRebateApplication.Status.APPROVED: + rebate_amount = kwargs.get("rebate_amount") + async_task( + "api.tasks.send_approve", instance.email, instance.id, rebate_amount + ) + # if application is not approved due to cra: + elif instance.status == GoElectricRebateApplication.Status.NOT_APPROVED: + async_task( + "api.tasks.send_not_approve", + instance.email, + instance.id, + instance.tax_year, + ) + elif instance.status == GoElectricRebateApplication.Status.CANCELLED: + async_task( + "api.tasks.send_cancel", + instance.email, + instance.id, + ) + + +@receiver(post_save, sender=GoElectricRebate) +def after_rebate_issued(sender, instance, created, **kwargs): + if created: + async_task( + "api.services.ncda.notify", + instance.drivers_licence, + instance.last_name, + instance.expiry_date.strftime("%m/%d/%Y"), + str(instance.rebate_max_amount), + instance.id, + ) diff --git a/django/api/signals.py b/django/api/signals.py index 1174b7ec..7a454624 100644 --- a/django/api/signals.py +++ b/django/api/signals.py @@ -1,92 +1,5 @@ -from django.db.models.signals import post_save -from .models.go_electric_rebate_application import ( - GoElectricRebateApplication, -) -from .models.household_member import HouseholdMember -from .models.go_electric_rebate import GoElectricRebate -from django.dispatch import receiver -from django.conf import settings -from api.models.household_member import HouseholdMember -from django_q.tasks import async_task -from api.utility import addresses_match +import django.dispatch +# custom signals -@receiver(post_save, sender=GoElectricRebateApplication) -def create_application(sender, instance, created, **kwargs): - if created and settings.EMAIL["SEND_EMAIL"]: - async_task("api.tasks.send_individual_confirm", instance.email, instance.id) - if instance.application_type == "household": - async_task( - "api.tasks.send_spouse_initial_message", - instance.spouse_email, - instance.id, - instance.email, - ) - - -@receiver(post_save, sender=HouseholdMember) -def after_household_member_save(sender, instance, created, **kwargs): - if created: - application = instance.application - primary_user = application.user - secondary_user = instance.user - if ( - primary_user.identity_provider == "bcsc" - and secondary_user.identity_provider == "bcsc" - and addresses_match(application, secondary_user) - ): - application.status = GoElectricRebateApplication.Status.VERIFIED - application.save() - else: - application.status = GoElectricRebateApplication.Status.SUBMITTED - application.save() - - if settings.EMAIL["SEND_EMAIL"]: - async_task( - "api.tasks.send_household_confirm", - application.email, - application.id, - ) - - -@receiver(post_save, sender=GoElectricRebateApplication) -def after_status_change(sender, instance, created, **kwargs): - if ( - (not created) - and (kwargs.get("update_fields") == {"status"}) - and (settings.EMAIL["SEND_EMAIL"]) - ): - # if identity is declined (eg id doesnt match address) - if instance.status == GoElectricRebateApplication.Status.DECLINED: - async_task( - "api.tasks.send_reject", - instance.email, - instance.id, - ) - # if rebate is approved, send an email with amount - elif instance.status == GoElectricRebateApplication.Status.APPROVED: - rebate_amount = kwargs.get("rebate_amount") - async_task( - "api.tasks.send_approve", instance.email, instance.id, rebate_amount - ) - # if application is not approved due to cra: - elif instance.status == GoElectricRebateApplication.Status.NOT_APPROVED: - async_task( - "api.tasks.send_not_approve", - instance.email, - instance.id, - instance.tax_year, - ) - - -@receiver(post_save, sender=GoElectricRebate) -def after_rebate_issued(sender, instance, created, **kwargs): - if created: - async_task( - "api.services.ncda.notify", - instance.drivers_licence, - instance.last_name, - instance.expiry_date.strftime("%m/%d/%Y"), - str(instance.rebate_max_amount), - instance.id, - ) +household_application_saved = django.dispatch.Signal() diff --git a/django/api/tasks.py b/django/api/tasks.py index d1968ecd..2ed50e71 100644 --- a/django/api/tasks.py +++ b/django/api/tasks.py @@ -6,12 +6,13 @@ from email.header import Header from email.utils import formataddr from requests.auth import HTTPBasicAuth -from django_q.tasks import schedule from api.services.ncda import get_rebates_redeemed_since from api.models.go_electric_rebate import GoElectricRebate from api.models.go_electric_rebate_application import ( GoElectricRebateApplication, ) +from django_q.models import Schedule +from datetime import timedelta def get_email_service_token() -> str: @@ -302,10 +303,50 @@ def send_not_approve(recipient_email, application_id, tax_year): ) +def send_cancel(recipient_email, application_id): + message = """\ + +
+ +This email was generated by the CleanBC Go Electric Passenger + Vehicle Rebate program application.
+ +Your application has been cancelled.
+ +Some examples of why this may have happened include:
+ +Questions?
+ +Please feel free to contact us at ZEVPrograms@gov.bc.ca
+ + + """ + send_email( + recipient_email, + application_id, + message, + cc_list=[], + optional_subject=" – Cancelled", + ) + + # check for newly redeemed rebates -# TODO schedule this task to automatically run. -def check_rebates_redeemed_since(iso_ts=None): - ts = iso_ts if iso_ts else timezone.now().strftime("%Y-%m-%dT00:00:00Z") +def check_rebates_redeemed_since(iso_ts=None, schedule_func_name=None): + ts = timezone.now().strftime("%Y-%m-%dT00:00:00Z") + if iso_ts: + ts = iso_ts + elif schedule_func_name: + schedule = Schedule.objects.get(func__exact=schedule_func_name) + ts = (schedule.next_run - timedelta(days=2)).strftime("%Y-%m-%dT%H:%M:%SZ") print("check_rebate_status " + ts) ncda_ids = get_rebates_redeemed_since(ts) print(ncda_ids) diff --git a/django/api/templates/admin/api/change_form.html b/django/api/templates/admin/api/change_form.html new file mode 100644 index 00000000..1c801089 --- /dev/null +++ b/django/api/templates/admin/api/change_form.html @@ -0,0 +1,18 @@ +{% extends "admin/change_form.html" %} {% load i18n admin_urls jazzmin %} {% +get_jazzmin_ui_tweaks as jazzmin_ui %} + + {% block field_sets %} +Return to the Go Electric site to learn about other rebate offers.
+ ); }; diff --git a/frontend/src/components/DetailsTable.js b/frontend/src/components/DetailsTable.js index 021893a0..0ab25caa 100644 --- a/frontend/src/components/DetailsTable.js +++ b/frontend/src/components/DetailsTable.js @@ -10,7 +10,7 @@ function createData(name, answer) { return { name, answer }; } -function createConsentValue(consent, firstName, lastName, timestamp, idp) { +function createConsentValue(consent, displayName, timestamp, idp) { const timestampSplit = timestamp.split('T'); const date = timestampSplit[0]; const time = timestampSplit[1].split('.')[0]; @@ -26,8 +26,7 @@ function createConsentValue(consent, firstName, lastName, timestamp, idp) { return ( authType + '\\' + - firstName.charAt(0).toUpperCase() + - lastName.toUpperCase() + + displayName + ' ' + date + ' ' + @@ -57,8 +56,7 @@ const DetailsTable = ({ data }) => { 'Consent to Disclosure and Storage of, and Access to, Personal Information:', createConsentValue( data.consent_personal, - data.first_name, - data.last_name, + data.displayName, data.created, data.idp ) @@ -67,8 +65,7 @@ const DetailsTable = ({ data }) => { 'Consent to Disclosure of Information from Income Tax Records:', createConsentValue( data.consent_tax, - data.first_name, - data.last_name, + data.displayName, data.created, data.idp ) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 613c404b..b2b94fa5 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -2,7 +2,7 @@ import React from 'react'; import logo from '../styles/images/BCID_H_rgb_rev.png'; import Logout from './Logout'; -const Header = (props) => { +const Header = ({ logoutUri }) => { return (+ You must use your own BC Services Card app or Basic BCeID account to + log in. +
++ You cannot use the same log in credentials as the applicant to + complete your household rebate application. +
++ Click the Log out button and log in using your own BC Services Card + app or Basic BCeID account. +
+ +Loading...
; + returnThis application has been cancelled
; } if (isError) { + const errorResponse = error.response; + if ( + errorResponse && + errorResponse.data && + errorResponse.data.error === 'same_user' + ) { + setSameUser({ + error: true, + logoutUri: `${window.location.origin}/household?q=${id}` + }); + } else if ( + errorResponse && + errorResponse.data && + errorResponse.data.error === 'application_cancelled' + ) { + setApplicationCancelled(true); + } return{error.message}
; } - const { address, city, postal_code: postalCode } = data; + const { address, city, postal_code: postalCode, status } = data; return (+ If you are unable to complete this application click Cancel + Application. This will notify the primary applicant and enable them + to start a new application. +
+ +