diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index ff3a0fd5..a9f6afc3 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -39,45 +39,45 @@ const phases = { instance: `${name}-dev-${changeId}` , version:`${version}-${changeId}`, tag:`dev-${version}-${changeId}`, host: `itvr-dev-${changeId}.${ocpName}.gov.bc.ca`, djangoDebug: 'True', logoutHostName: 'logontest.gov.bc.ca', metabaseCpuRequest: '200m', metabaseCpuLimit: '300m', metabaseMemoryRequest: '500Mi', metabaseMemoryLimit: '2Gi', metabaseReplicas: 1, - frontendCpuRequest: '70m', frontendCpuLimit: '210m', frontendMemoryRequest: '300Mi', frontendMemoryLimit: '600Mi', frontendReplicas: 1, + frontendCpuRequest: '30m', frontendCpuLimit: '60m', frontendMemoryRequest: '30Mi', frontendMemoryLimit: '60Mi', frontendReplicas: 1, reactAppBCSCKeycloakClientId: 'itvr', reactAppBCSCKeycloakRealm: 'rzh2zkjq', reactAppBCSCKeycloakUrl: 'https://dev.oidc.gov.bc.ca/auth/', reactAppApiBase: `https://itvr-backend-dev-${changeId}.apps.silver.devops.gov.bc.ca`, reactAppBCeIDKeycloakClientId: 'itvr-2674', reactAppBCeIDKeycloakRealm: 'onestopauth-basic', reactAppBCeIDKeycloakUrl: 'https://dev.oidc.gov.bc.ca/auth/', backendCpuRequest: '60m', backendCpuLimit: '120m', backendMemoryRequest: '120Mi', backendMemoryLimit: '240Mi', backendHealthCheckDelay: 30, backendHost: `itvr-backend-dev-${changeId}.${ocpName}.gov.bc.ca`, backendReplicas: 1, backendDjangoDebug: 'True', bucketName: 'itvrdv', minioCpuRequest: '30m', minioCpuLimit: '100m', minioMemoryRequest: '150Mi', minioMemoryLimit: '300Mi', minioPvcSize: '3Gi', schemaspyCpuRequest: '50m', schemaspyCpuLimit: '200m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, rabbitmqCpuRequest: '250m', rabbitmqCpuLimit: '700m', rabbitmqMemoryRequest: '500M', rabbitmqMemoryLimit: '1G', rabbitmqPvcSize: '1G', rabbitmqReplica: 1, rabbitmqPostStartSleep: 120, storageClass: 'netapp-block-standard', - patroniCpuRequest: '30m', patroniCpuLimit: '60m', patroniMemoryRequest: '80Mi', patroniMemoryLimit: '160Mi', patroniPvcSize: '2G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, - taskQueueCpuRequest: '40m', taskQueueCpuLimit: '120m', taskQueueMemoryRequest: '120Mi', taskQueueMemoryLimit: '240Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'True',}, + patroniCpuRequest: '60m', patroniCpuLimit: '120m', patroniMemoryRequest: '200Mi', patroniMemoryLimit: '400Mi', patroniPvcSize: '2G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, + taskQueueCpuRequest: '60m', taskQueueCpuLimit: '120m', taskQueueMemoryRequest: '200Mi', taskQueueMemoryLimit: '400Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'True',}, test: {namespace:'ac294c-test', name: `${name}`, ssoSuffix:'-test', ssoName:'test.oidc.gov.bc.ca', phase: 'test' , changeId:`${changeId}`, suffix: `-test`, instance: `${name}-test`, version:`${version}`, tag:`test-${version}`, host: `itvr-test.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHostName: 'logontest.gov.bc.ca', metabaseCpuRequest: '200m', metabaseCpuLimit: '300m', metabaseMemoryRequest: '500Mi', metabaseMemoryLimit: '2Gi', metabaseReplicas: 1, - frontendCpuRequest: '70m', frontendCpuLimit: '210m', frontendMemoryRequest: '300Mi', frontendMemoryLimit: '600Mi', frontendReplicas: 2, frontendMinReplicas: 1, frontendMaxReplicas: 3, + frontendCpuRequest: '30m', frontendCpuLimit: '60m', frontendMemoryRequest: '30Mi', frontendMemoryLimit: '60Mi', frontendReplicas: 2, frontendMinReplicas: 1, frontendMaxReplicas: 3, reactAppBCSCKeycloakClientId: 'itvr', reactAppBCSCKeycloakRealm: 'rzh2zkjq', reactAppBCSCKeycloakUrl: 'https://test.oidc.gov.bc.ca/auth/', reactAppApiBase: `https://itvr-backend-test.apps.silver.devops.gov.bc.ca`, reactAppBCeIDKeycloakClientId: 'itvr-2674', reactAppBCeIDKeycloakRealm: 'onestopauth-basic', reactAppBCeIDKeycloakUrl: 'https://test.oidc.gov.bc.ca/auth/', - backendCpuRequest: '40m', backendCpuLimit: '120m', backendMemoryRequest: '120Mi', backendMemoryLimit: '240Mi', backendHealthCheckDelay: 30, backendReplicas: 2, backendMinReplicas: 1, backendMaxReplicas: 3, backendHost: `itvr-backend-test.${ocpName}.gov.bc.ca`, backendDjangoDebug: 'False', bucketName: 'itvrts', + backendCpuRequest: '30m', backendCpuLimit: '60m', backendMemoryRequest: '120Mi', backendMemoryLimit: '240Mi', backendHealthCheckDelay: 30, backendReplicas: 2, backendMinReplicas: 1, backendMaxReplicas: 3, backendHost: `itvr-backend-test.${ocpName}.gov.bc.ca`, backendDjangoDebug: 'False', bucketName: 'itvrts', minioCpuRequest: '30m', minioCpuLimit: '100m', minioMemoryRequest: '150Mi', minioMemoryLimit: '300Mi', minioPvcSize: '3G', schemaspyCpuRequest: '20m', schemaspyCpuLimit: '200m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, rabbitmqCpuRequest: '250m', rabbitmqCpuLimit: '700m', rabbitmqMemoryRequest: '500M', rabbitmqMemoryLimit: '700M', rabbitmqPvcSize: '1G', rabbitmqReplica: 2, rabbitmqPostStartSleep: 120, storageClass: 'netapp-block-standard', - patroniCpuRequest: '200m', patroniCpuLimit: '400m', patroniMemoryRequest: '250Mi', patroniMemoryLimit: '500Mi', patroniPvcSize: '5G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, - taskQueueCpuRequest: '40m', taskQueueCpuLimit: '120m', taskQueueMemoryRequest: '120Mi', taskQueueMemoryLimit: '240Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'False',}, + patroniCpuRequest: '60m', patroniCpuLimit: '120m', patroniMemoryRequest: '200Mi', patroniMemoryLimit: '400Mi', patroniPvcSize: '5G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, + taskQueueCpuRequest: '60m', taskQueueCpuLimit: '120m', taskQueueMemoryRequest: '200Mi', taskQueueMemoryLimit: '400Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'False',}, prod: {namespace:'ac294c-prod', name: `${name}`, ssoSuffix:'', ssoName:'oidc.gov.bc.ca', phase: 'prod' , changeId:`${changeId}`, suffix: `-prod`, instance: `${name}-prod`, version:`${version}`, tag:`prod-${version}`, metabaseCpuRequest: '200m', metabaseCpuLimit: '300m', metabaseMemoryRequest: '500Mi', metabaseMemoryLimit: '2Gi', metabaseReplicas: 1, host: `itvr-prod.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHostName: 'logon7.gov.bc.ca', - frontendCpuRequest: '140m', frontendCpuLimit: '280m', frontendMemoryRequest: '600Mi', frontendMemoryLimit: '1200Mi', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5, + frontendCpuRequest: '30m', frontendCpuLimit: '60m', frontendMemoryRequest: '30Mi', frontendMemoryLimit: '60Mi', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5, reactAppBCSCKeycloakClientId: 'itvr', reactAppBCSCKeycloakRealm: 'rzh2zkjq', reactAppBCSCKeycloakUrl: 'https://oidc.gov.bc.ca/auth/', reactAppApiBase: `https://itvr-backend-prod.apps.silver.devops.gov.bc.ca`, reactAppBCeIDKeycloakClientId: 'itvr-2674', reactAppBCeIDKeycloakRealm: 'onestopauth-basic', reactAppBCeIDKeycloakUrl: 'https://oidc.gov.bc.ca/auth/', - backendCpuRequest: '80m', backendCpuLimit: '160m', backendMemoryRequest: '240Mi', backendMemoryLimit: '480Mi', backendHealthCheckDelay: 30, backendReplicas: 3, backendMinReplicas: 3, backendMaxReplicas: 5, backendHost: `itvr-backend-prod.${ocpName}.gov.bc.ca`, backendDjangoDebug: 'False', bucketName: 'itvrpr', + backendCpuRequest: '30m', backendCpuLimit: '60m', backendMemoryRequest: '240Mi', backendMemoryLimit: '480Mi', backendHealthCheckDelay: 30, backendReplicas: 3, backendMinReplicas: 3, backendMaxReplicas: 5, backendHost: `itvr-backend-prod.${ocpName}.gov.bc.ca`, backendDjangoDebug: 'False', bucketName: 'itvrpr', minioCpuRequest: '30m', minioCpuLimit: '100m', minioMemoryRequest: '150Mi', minioMemoryLimit: '300Mi', minioPvcSize: '3G', schemaspyCpuRequest: '50m', schemaspyCpuLimit: '400m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, rabbitmqCpuRequest: '250m', rabbitmqCpuLimit: '700m', rabbitmqMemoryRequest: '500M', rabbitmqMemoryLimit: '1G', rabbitmqPvcSize: '5G', rabbitmqReplica: 2, rabbitmqPostStartSleep: 120, storageClass: 'netapp-block-standard', - patroniCpuRequest: '200m', patroniCpuLimit: '400m', patroniMemoryRequest: '250Mi', patroniMemoryLimit: '500Mi', patroniPvcSize: '8G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, - taskQueueCpuRequest: '80m', taskQueueCpuLimit: '160m', taskQueueMemoryRequest: '150Mi', taskQueueMemoryLimit: '300Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'False',} + patroniCpuRequest: '60m', patroniCpuLimit: '120m', patroniMemoryRequest: '200Mi', patroniMemoryLimit: '400Mi', patroniPvcSize: '5G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`, + taskQueueCpuRequest: '60m', taskQueueCpuLimit: '120m', taskQueueMemoryRequest: '200Mi', taskQueueMemoryLimit: '400Mi', taskQueueReplicas: 1, taskQueueDjangoDebug: 'False',} }; diff --git a/chart/itvr/charts/itvr-spilo/values-prod.yaml b/chart/itvr/charts/itvr-spilo/values-prod.yaml index 70dceac4..3e822b66 100644 --- a/chart/itvr/charts/itvr-spilo/values-prod.yaml +++ b/chart/itvr/charts/itvr-spilo/values-prod.yaml @@ -31,11 +31,11 @@ spilo: resources: limits: - cpu: 240m - memory: 800Mi - requests: cpu: 120m - memory: 400Mi + memory: 400Mi + requests: + cpu: 60m + memory: 200Mi podDisruptionBudget: enabled: false diff --git a/django/api/admin.py b/django/api/admin.py index 0408a1a6..89bed27f 100644 --- a/django/api/admin.py +++ b/django/api/admin.py @@ -4,6 +4,7 @@ SubmittedGoElectricRebateApplication, ) from .models.household_member import HouseholdMember +from .models.go_electric_rebate import GoElectricRebate class HouseholdApplicationInline(admin.StackedInline): @@ -15,6 +16,9 @@ class HouseholdApplicationInline(admin.StackedInline): "first_name", "middle_names", "date_of_birth", + "bcsc_address", + "bcsc_city", + "bcsc_postal_code", "doc1_tag", "doc2_tag", "consent_personal", @@ -89,3 +93,8 @@ def response_change(self, request, obj): obj.status = GoElectricRebateApplication.Status.DECLINED obj.save(update_fields=["status"]) return ret + + +@admin.register(GoElectricRebate) +class GoElectricRebateAdmin(admin.ModelAdmin): + pass diff --git a/django/api/migrations/0001_initial.py b/django/api/migrations/0001_initial.py index 7e46489a..e24b29c8 100644 --- a/django/api/migrations/0001_initial.py +++ b/django/api/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.0.1 on 2022-06-12 23:01 +# Generated by Django 4.0.1 on 2022-06-21 17:05 import api.validators from django.conf import settings @@ -59,6 +59,9 @@ class Migration(migrations.Migration): ('first_name', models.CharField(max_length=250)), ('middle_names', models.CharField(blank=True, max_length=250, null=True)), ('date_of_birth', models.DateField(validators=[api.validators.validate_driving_age])), + ('bcsc_address', models.CharField(blank=True, max_length=250, null=True)), + ('bcsc_city', models.CharField(blank=True, max_length=250, null=True)), + ('bcsc_postal_code', models.CharField(blank=True, max_length=6, null=True)), ('doc1', models.ImageField(blank=True, null=True, upload_to='docs')), ('doc2', models.ImageField(blank=True, null=True, upload_to='docs')), ('consent_personal', models.BooleanField(validators=[api.validators.validate_consent])), @@ -73,14 +76,15 @@ class Migration(migrations.Migration): migrations.CreateModel( name='GoElectricRebate', fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), - ('id', models.AutoField(primary_key=True, serialize=False)), - ('drivers_licence', models.CharField(max_length=8, validators=[django.core.validators.MinLengthValidator(7)])), + ('drivers_licence', models.CharField(max_length=8, unique=True, validators=[django.core.validators.MinLengthValidator(7)])), ('last_name', models.CharField(max_length=250)), ('expiry_date', models.DateField()), ('rebate_max_amount', models.IntegerField(default=0)), - ('rebate_state', models.BooleanField(default=False)), + ('redeemed', models.BooleanField(default=False)), + ('ncda_id', models.IntegerField(blank=True, null=True)), ('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='api.goelectricrebateapplication')), ], options={ @@ -99,4 +103,8 @@ class Migration(migrations.Migration): }, bases=('api.goelectricrebateapplication',), ), + migrations.AddConstraint( + model_name='goelectricrebateapplication', + constraint=models.UniqueConstraint(condition=models.Q(('status__in', ['household_initiated', 'submitted', 'approved', 'redeemed', 'verified'])), fields=('drivers_licence',), name='verify_rebate_status'), + ), ] diff --git a/django/api/models/go_electric_rebate.py b/django/api/models/go_electric_rebate.py index fddbd2f9..1ea82b68 100644 --- a/django/api/models/go_electric_rebate.py +++ b/django/api/models/go_electric_rebate.py @@ -5,25 +5,29 @@ BooleanField, PROTECT, ForeignKey, - AutoField, + UUIDField, ) from django.core.validators import MinLengthValidator - from django_extensions.db.models import TimeStampedModel from django.utils.translation import gettext_lazy as _ from api.models.go_electric_rebate_application import GoElectricRebateApplication class GoElectricRebate(TimeStampedModel): - id = AutoField(primary_key=True) application = ForeignKey( GoElectricRebateApplication, on_delete=PROTECT, blank=True, null=True ) drivers_licence = CharField( - max_length=8, unique=False, validators=[MinLengthValidator(7)] + max_length=8, unique=True, validators=[MinLengthValidator(7)] ) last_name = CharField(max_length=250, unique=False) expiry_date = DateField() rebate_max_amount = IntegerField(default=0) - rebate_state = BooleanField(default=False) + redeemed = BooleanField(default=False) + # sharepoint id. If something goes wrong with notification we can find + # issued rebates with blank NCDA ids to try resending. + ncda_id = IntegerField(blank=True, null=True) + + def __str__(self): + return "DL: " + self.drivers_licence + ", $" + str(self.rebate_max_amount) diff --git a/django/api/models/go_electric_rebate_application.py b/django/api/models/go_electric_rebate_application.py index 6f2e8aa1..fa88720a 100644 --- a/django/api/models/go_electric_rebate_application.py +++ b/django/api/models/go_electric_rebate_application.py @@ -11,6 +11,8 @@ ForeignKey, TextChoices, Manager, + Q, + UniqueConstraint, ) from encrypted_fields.fields import EncryptedCharField from django.utils.html import mark_safe @@ -103,6 +105,21 @@ def __str__(self): class Meta: db_table = "go_electric_rebate_application" + constraints = [ + UniqueConstraint( + fields=["drivers_licence"], + condition=Q( + status__in=[ + "household_initiated", + "submitted", + "approved", + "redeemed", + "verified", + ] + ), + name="verify_rebate_status", + ) + ] # This is for the admin panel diff --git a/django/api/models/household_member.py b/django/api/models/household_member.py index a7f2abe7..fd015038 100644 --- a/django/api/models/household_member.py +++ b/django/api/models/household_member.py @@ -35,6 +35,9 @@ class HouseholdMember(TimeStampedModel): first_name = CharField(max_length=250, unique=False) middle_names = CharField(max_length=250, unique=False, blank=True, null=True) date_of_birth = DateField(validators=[validate_driving_age]) + 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) def doc1_tag(self): diff --git a/django/api/serializers/application_form.py b/django/api/serializers/application_form.py index 2c35e773..6649575d 100644 --- a/django/api/serializers/application_form.py +++ b/django/api/serializers/application_form.py @@ -4,7 +4,8 @@ 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 = ( @@ -88,25 +89,28 @@ def create(self, validated_data): user = request.user spouse_email = request.data.get("spouse_email") - obj = GoElectricRebateApplication.objects.create( - sin=validated_data["sin"], - status=self._get_status(validated_data), - email=validated_data["email"], - drivers_licence=validated_data["drivers_licence"], - last_name=user.last_name, - first_name=user.first_name, - date_of_birth=user.date_of_birth, - address=user.street_address, - city=user.locality, - postal_code=user.postal_code, - tax_year=self._get_tax_year(), - application_type=validated_data["application_type"], - spouse_email=spouse_email, - user=user, - consent_personal=validated_data["consent_personal"], - consent_tax=validated_data["consent_tax"], - ) - return obj + try: + obj = GoElectricRebateApplication.objects.create( + sin=validated_data["sin"], + status=self._get_status(validated_data), + email=validated_data["email"], + drivers_licence=validated_data["drivers_licence"], + last_name=user.last_name, + first_name=user.first_name, + date_of_birth=user.date_of_birth, + address=user.street_address, + city=user.locality, + postal_code=user.postal_code, + tax_year=self._get_tax_year(), + application_type=validated_data["application_type"], + spouse_email=spouse_email, + user=user, + consent_personal=validated_data["consent_personal"], + consent_tax=validated_data["consent_tax"], + ) + return obj + except Exception as e: + return Response({"response": str(e)}, status=status.HTTP_400_BAD_REQUEST) def _get_status(self, validated_data): application_type = validated_data["application_type"] diff --git a/django/api/serializers/household_member.py b/django/api/serializers/household_member.py index a0ac2559..06f15ca8 100644 --- a/django/api/serializers/household_member.py +++ b/django/api/serializers/household_member.py @@ -13,7 +13,7 @@ class HouseholdMemberApplicationCreateSerializer(ModelSerializer): class Meta: model = HouseholdMember - exclude = ["user"] + exclude = ["user", "bcsc_address", "bcsc_city", "bcsc_postal_code"] class HouseholdMemberApplicationCreateSerializerDefault( @@ -67,6 +67,9 @@ def create(self, validated_data): last_name=user.last_name, first_name=user.first_name, date_of_birth=user.date_of_birth, + bcsc_address=user.street_address, + bcsc_city=user.locality, + bcsc_postal_code=user.postal_code, user=user, consent_personal=validated_data["consent_personal"], consent_tax=validated_data["consent_tax"], diff --git a/django/api/services/ncda.py b/django/api/services/ncda.py new file mode 100644 index 00000000..b6dbf5de --- /dev/null +++ b/django/api/services/ncda.py @@ -0,0 +1,184 @@ +import requests +import json +from django.conf import settings +from api.models.go_electric_rebate import GoElectricRebate + + +def get_ncda_service_token() -> str: + client_id = settings.NCDA_CLIENT_ID + client_secret = settings.NCDA_CLIENT_SECRET + resource = settings.NCDA_RESOURCE + url = settings.NCDA_AUTH_URL + payload = { + "grant_type": "client_credentials", + "client_credentials": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "resource": resource, + } + + headers = {"content-type": "application/x-www-form-urlencoded"} + + token_rs = requests.post( + url, + data=payload, + headers=headers, + verify=True, + ) + token_rs.raise_for_status() + return token_rs.json()["access_token"] + + +# Tell NCDA about a newly issued rebate. +def notify(drivers_licence, last_name, expiry_date, rebate_amount, rebate_id): + api_endpoint = settings.NCDA_SHAREPOINT_URL + access_token = get_ncda_service_token() + + # { + # "__metadata": {"type": "SP.Data.ITVREligibilityListItem"}, + # "Title": "77777777", + # "LastName": "Test7", + # "ExpiryDT": "6/22/2023", + # "MaxRebateAmt": "1500", + # "Status": "Not-Redeemed", + # } + payload = json.dumps( + { + "__metadata": {"type": "SP.Data.ITVREligibilityListItem"}, + "Title": drivers_licence, + "LastName": last_name, + "ExpiryDT": expiry_date, + "MaxRebateAmt": rebate_amount, + "Status": "Not-Redeemed", + } + ) + + headers = { + "Authorization": "Bearer " + access_token, + "Accept": "application/json;odata=verbose", + "Content-Type": "application/json;odata=verbose", + } + + url = api_endpoint + "/lists/getbytitle('ITVREligibility')/items" + + ncda_rs = requests.post( + url, + data=payload, + headers=headers, + verify=True, + ) + + # { + # "d": { + # "__metadata": { + # "id": "57bc90e6-6774-416e-8daa-090385ef8f45", + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)", + # "etag": '"1"', + # "type": "SP.Data.ITVREligibilityListItem", + # }, + # "FirstUniqueAncestorSecurableObject": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/FirstUniqueAncestorSecurableObject" + # } + # }, + # "RoleAssignments": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/RoleAssignments" + # } + # }, + # "AttachmentFiles": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/AttachmentFiles" + # } + # }, + # "ContentType": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/ContentType" + # } + # }, + # "GetDlpPolicyTip": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/GetDlpPolicyTip" + # } + # }, + # "FieldValuesAsHtml": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/FieldValuesAsHtml" + # } + # }, + # "FieldValuesAsText": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/FieldValuesAsText" + # } + # }, + # "FieldValuesForEdit": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/FieldValuesForEdit" + # } + # }, + # "File": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/File" + # } + # }, + # "Folder": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/Folder" + # } + # }, + # "LikedByInformation": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/LikedByInformation" + # } + # }, + # "ParentList": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/ParentList" + # } + # }, + # "Properties": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/Properties" + # } + # }, + # "Versions": { + # "__deferred": { + # "uri": "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/Web/Lists(guid'7aea54d3-9935-40a4-88cd-3ddd74c7d270')/Items(19)/Versions" + # } + # }, + # "FileSystemObjectType": 0, + # "Id": 19, + # "ServerRedirectedEmbedUri": null, + # "ServerRedirectedEmbedUrl": "", + # "ID": 19, + # "ContentTypeId": "0x01008074AAC5CAD1A241B1109194FE6D8D5D00747BB541D61F1F4CA7028C9C97E823CB", + # "Title": "23456781", + # "Modified": "2022-06-16T22:51:39Z", + # "Created": "2022-06-16T22:51:39Z", + # "AuthorId": 1073741822, + # "EditorId": 1073741822, + # "OData__UIVersionString": "1.0", + # "Attachments": false, + # "GUID": "9e4bceff-29c5-41d0-889a-c320f32b64da", + # "ComplianceAssetId": null, + # "LastName": "Aro", + # "ExpiryDT": "2023-06-16T07:00:00Z", + # "MaxRebateAmt": 1400, + # "Status": "Not-Redeemed", + # "ClaimType": null, + # "OData__vti_ItemDeclaredRecord": null, + # } + # } + + # 500 Server Error: Internal Server Error for url: + # is sent for duplicate driver's license no etc. Might come up in dev + # Our unique index on driver's license in the rebate table + # should prevent this in prod. + print(ncda_rs.text) + + ncda_rs.raise_for_status() + + data = ncda_rs.json() + ncda_id = data["d"]["ID"] + + GoElectricRebate.objects.filter(pk=rebate_id).update(ncda_id=ncda_id) diff --git a/django/api/services/rebate.py b/django/api/services/rebate.py index 1069b7f7..12357be7 100644 --- a/django/api/services/rebate.py +++ b/django/api/services/rebate.py @@ -7,15 +7,21 @@ # gets applications from rebates def get_applications(rebates): + result = {} ids = [] if rebates is not None: ids = list(rebates) - return GoElectricRebateApplication.objects.in_bulk(ids) + applications = GoElectricRebateApplication.objects.filter(id__in=ids).filter( + status__exact=GoElectricRebateApplication.Status.VERIFIED + ) + for application in applications: + result[application.id] = application + return result # saves approved rebates to the rebate table; returns the saved rebates def save_rebates(rebates, applications): - result = [] + created_rebates = [] if rebates is not None and applications is not None: rebate_objs = [] for application_id, rebate_amount in rebates.items(): @@ -28,11 +34,17 @@ def save_rebates(rebates, applications): last_name=application.last_name, expiry_date=date.today() + timedelta(days=365), rebate_max_amount=rebate_amount, - rebate_state=False, + redeemed=False, ) rebate_objs.append(rebate_obj) - result = GoElectricRebate.objects.bulk_create(rebate_objs) - return result + created_rebates = GoElectricRebate.objects.bulk_create(rebate_objs) + for rebate in created_rebates: + post_save.send( + sender=GoElectricRebate, + instance=rebate, + created=True, + ) + return created_rebates # updates application statuses; emits signals manually diff --git a/django/api/services/tests/test_issue_rebate.py b/django/api/services/tests/test_issue_rebate.py index c1fc7314..54637816 100644 --- a/django/api/services/tests/test_issue_rebate.py +++ b/django/api/services/tests/test_issue_rebate.py @@ -14,6 +14,11 @@ class TestIssueRebate(TestRebate): @classmethod def setUpClass(self): super().setUpClass() + applications = GoElectricRebateApplication.objects.all() + for application in applications: + application.status = GoElectricRebateApplication.Status.VERIFIED + GoElectricRebateApplication.objects.bulk_update(applications, ["status"]) + self.rebates = { "9uXLvNQS5vkKnscD": 2000, "B5t92XeH7NnFUwxc": 4000, diff --git a/django/api/services/tests/test_rebate.py b/django/api/services/tests/test_rebate.py index 657825aa..38046483 100644 --- a/django/api/services/tests/test_rebate.py +++ b/django/api/services/tests/test_rebate.py @@ -49,7 +49,7 @@ def setUpClass(self): city="Victoria", postal_code="v8s4j9", tax_year=2020, - drivers_licence="1234567", + drivers_licence="1234568", ) GoElectricRebateApplication.objects.create( id="ctW8gU57YX4xfQ9o", @@ -68,7 +68,7 @@ def setUpClass(self): city="Victoria", postal_code="v8s4j9", tax_year=2020, - drivers_licence="1234567", + drivers_licence="1234569", ) HouseholdMember.objects.create( application_id="9uXLvNQS5vkKnscD", diff --git a/django/api/settings.py b/django/api/settings.py index e4fd6f3d..20fcb24e 100644 --- a/django/api/settings.py +++ b/django/api/settings.py @@ -202,7 +202,7 @@ "queue_limit": 50, "bulk": 10, "orm": "default", - "save_limit": -1, + "save_limit": 20 if DEBUG else -1, } CACHES = { @@ -230,3 +230,22 @@ "B": {"individual_income": 90000, "household_income": 145000, "rebate": 2000}, "A": {"individual_income": 80000, "household_income": 125000, "rebate": 4000}, } + +# NCDA Sharepoint config +NCDA_CLIENT_ID = os.getenv( + "NCDA_CLIENT_ID", + "d4d97d40-bb26-44f8-ba70-c677471d6cc1@1d4864aa-f2da-42dc-a62a-34b4dd790b6a", +) +NCDA_CLIENT_SECRET = os.getenv("NCDA_CLIENT_SECRET") +NCDA_RESOURCE = os.getenv( + "NCDA_RESOURCE", + "00000003-0000-0ff1-ce00-000000000000/newcardealers.sharepoint.com@1d4864aa-f2da-42dc-a62a-34b4dd790b6a", +) +NCDA_AUTH_URL = os.getenv( + "NCDA_AUTH_URL", + "https://accounts.accesscontrol.windows.net/1d4864aa-f2da-42dc-a62a-34b4dd790b6a/tokens/OAuth/2/", +) +NCDA_SHAREPOINT_URL = os.getenv( + "NCDA_SHAREPOINT_URL", + "https://newcardealers.sharepoint.com/sites/ElectricVehicleRebateApplications/_api/web", +) diff --git a/django/api/signals.py b/django/api/signals.py index a3fcfad8..1174b7ec 100644 --- a/django/api/signals.py +++ b/django/api/signals.py @@ -1,6 +1,9 @@ from django.db.models.signals import post_save -from .models.go_electric_rebate_application import GoElectricRebateApplication +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 @@ -74,3 +77,16 @@ def after_status_change(sender, instance, created, **kwargs): 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, + ) diff --git a/django/api/tasks.py b/django/api/tasks.py index 34e9d513..0bb407f7 100644 --- a/django/api/tasks.py +++ b/django/api/tasks.py @@ -78,9 +78,11 @@ def send_individual_confirm(recipient_email, application_id): Passenger Vehicle Rebate program application.
+Thank you.
+We have received your application for a rebate under the CleanBC Go - Electric Passenger Vehicle Rebate program. + Electric Passenger Vehicle Rebate program. You can expect to get an email reply with the result of your application within 3 weeks.
Please keep this e-mail for your records.
@@ -100,13 +102,6 @@ def send_spouse_initial_message(recipient_email, application_id, initiator_email -- This email was generated by the CleanBC Go Electric - Passenger Vehicle Rebate program application. -
- -Dear Applicant,
-You are receiving this e-mail as you have been identified as a spouse under a household rebate application for the CleanBC Go @@ -120,11 +115,11 @@ def send_spouse_initial_message(recipient_email, application_id, initiator_email
{origin}/household?q={application_id}
-+
If you are not the intended person to receive this email, please contact the CleanBC Go Electric Passenger Vehicle Rebate program at ZEVPrograms@gov.bc.ca -
+Additional Questions?
@@ -152,7 +147,7 @@ def send_household_confirm(recipient_email, application_id):We have now received all documentation for your application for a household rebate under the CleanBC Go Electric Passenger Vehicle - Rebate program. + Rebate program. You can expect to get an email reply with the result of your application within 3 weeks.
Please keep this e-mail for your records.
@@ -175,14 +170,9 @@ def send_reject(recipient_email, application_id):This email was generated by the CleanBC Go Electric Passenger Vehicle Rebate program application.
-Dear Applicant,
- -You are receiving this e-mail as a response to your application for - a rebate under the CleanBC Go Electric Passenger Vehicle Rebate - program.
+Dear Applicant,
-We would like to notify you, that your application has not been - approved.
+Your application has not been approved.
Some examples of why this may have happened include:
@@ -196,6 +186,8 @@ def send_reject(recipient_email, application_id): +You are encouraged to correct these issues and submit another application.
+Questions?
Please feel free to contact us at ZEVPrograms@gov.bc.ca
@@ -219,34 +211,29 @@ def send_approve(recipient_email, application_id, rebate_amount):This email was generated by the CleanBC Go Electric Passenger Vehicle Rebate program application.
-Dear Applicant,
- -You are receiving this e-mail as a response to your application for - a rebate under the CleanBC Go Electric Passenger Vehicle Rebate - program.
+Dear Applicant,
-We would like to notify you, that your application has been approved - and you are entitled to a maximum rebate amount of ${rebate_amount}.
++ Your application has been approved for a rebate amount of up to ${rebate_amount}. + The full amount applies to purchases of battery electric and long-range plug-in hybrids. + For plug-in hybrids with ranges less than 85 km the rebate amount is half. +
Your rebate will expire one year from today’s date.
Next steps:
Please note: This e-mail confirms that you have been approved for a +
Please note: This e-mail confirms that you have been approved for a rebate under the CleanBC Go Electric Light-Duty Vehicle program only. Accessing the rebate is conditional on Program funds being available - at the time of vehicle purchase.
+ at the time of vehicle purchase.Questions?
@@ -273,13 +260,9 @@ def send_not_approve(recipient_email, application_id, tax_year):This email was generated by the CleanBC Go Electric Passenger Vehicle Rebate program application.
-Dear Applicant,
- -You are receiving this e-mail as a response to your application for - a rebate under the CleanBC Go Electric Passenger Vehicle Rebate - program.
+Dear Applicant,
-We would like to notify you, that your application has not been approved.
+Your application has not been approved.
Some examples of why this may have happened include:
diff --git a/django/api/templates/admin/api/submittedgoelectricrebateapplication/change_list.html b/django/api/templates/admin/api/submittedgoelectricrebateapplication/change_list.html new file mode 100644 index 00000000..a656dc8e --- /dev/null +++ b/django/api/templates/admin/api/submittedgoelectricrebateapplication/change_list.html @@ -0,0 +1,6 @@ +{% extends "admin/change_list.html" %} {% load i18n admin_urls jazzmin %} {% +get_jazzmin_ui_tweaks as jazzmin_ui %} + + +{% block content_title %} Submitted Rebate Applications {% endblock %} + diff --git a/django/api/templates/admin/api/submittedgoelectricrebateapplication/submit_line.html b/django/api/templates/admin/api/submittedgoelectricrebateapplication/submit_line.html index c531f5e9..7a8c126f 100644 --- a/django/api/templates/admin/api/submittedgoelectricrebateapplication/submit_line.html +++ b/django/api/templates/admin/api/submittedgoelectricrebateapplication/submit_line.html @@ -14,7 +14,7 @@Not a valid B.C. Driver's Licence Number
)} + {!submitStatus && ( + + Error: This driver's licence number has already been submitted or + issued a rebate. + + )}Used to connect you with your rebate.
-Used to confirm your identity.
The{' '} @@ -24,17 +24,7 @@ const IndividualLogin = () => { is the simplest method to log in and confirm your identity.
-- You can also log in with a{' '} - - Basic BCeID account - - . If you log in with BCeID you will need to upload images of your BC - Driver’s Licence and a secondary piece of ID.{' '} - - Learn more about ID requirements. - -
+For a household application your spouse or common law partner will @@ -43,9 +33,10 @@ const IndividualLogin = () => {