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:

  1. - Please allow X business days for your information to be populated into - the rebate database at B.C. automotive dealerships.
  2. -
  3. - Bring your drivers license with you to an automotive dealer in B.C. + Your approval is now linked to your driver’s licence. Bring your driver's licence with you to a new car dealer in B.C.
  4. - Use your rebate at the time of vehicle purchase to realize cost savings - on your new zero-emission vehicle! + Claim your rebate at the time of vehicle purchase to save money on your new zero-emission vehicle!
-

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 @@

@@ -23,7 +23,7 @@

diff --git a/django/api/viewsets/application_form.py b/django/api/viewsets/application_form.py index ef004f13..e2860acd 100644 --- a/django/api/viewsets/application_form.py +++ b/django/api/viewsets/application_form.py @@ -39,3 +39,26 @@ def get_serializer_class(self): return ApplicationFormCreateSerializerBCSC return ApplicationFormCreateSerializerDefault return ApplicationFormSerializer + + @action(detail=False, methods=["GET"], url_path="check_status") + def check_status(self, request, pk=None): + drivers_licence = request.query_params.get("drivers_license", None) + dl_not_valid = ( + GoElectricRebateApplication.objects.filter( + drivers_licence__exact=drivers_licence + ) + .filter( + status__in=[ + GoElectricRebateApplication.Status.SUBMITTED, + GoElectricRebateApplication.Status.HOUSEHOLD_INITIATED, + GoElectricRebateApplication.Status.VERIFIED, + GoElectricRebateApplication.Status.APPROVED, + GoElectricRebateApplication.Status.REDEEMED, + ] + ) + .exists() + ) + + if dl_not_valid: + return Response({"drivers_license_valid": "false"}) + return Response({"drivers_license_valid": "true"}) diff --git a/docker-compose.yml b/docker-compose.yml index a8018587..4db019e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,6 +60,7 @@ services: - CHES_EMAIL_URL - SEND_EMAIL - BYPASS_AUTHENTICATION + - NCDA_CLIENT_SECRET volumes: - ./django:/api ports: @@ -87,6 +88,7 @@ services: - CHES_AUTH_URL - CHES_EMAIL_URL - SEND_EMAIL + - NCDA_CLIENT_SECRET volumes: - ./django:/api depends_on: diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 521c197d..232854b0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,23 +1,26 @@ { "name": "frontend", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "frontend", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { + "@date-io/date-fns": "^2.14.0", "@emotion/react": "^11.6.0", "@emotion/styled": "^11.6.0", "@mui/icons-material": "^5.4.4", "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", + "@mui/x-date-pickers": "^5.0.0-alpha.6", "@react-keycloak/web": "^3.4.0", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^13.5.0", "axios": "^0.26.0", + "date-fns": "^2.28.0", "jwt-decode": "^3.1.2", "keycloak-js": "^17.0.0", "prop-types": "^15.8.1", @@ -2202,6 +2205,75 @@ "postcss": "^8.3" } }, + "node_modules/@date-io/core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.14.0.tgz", + "integrity": "sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw==" + }, + "node_modules/@date-io/date-fns": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + } + } + }, + "node_modules/@date-io/dayjs": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.14.0.tgz", + "integrity": "sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "dayjs": "^1.8.17" + }, + "peerDependenciesMeta": { + "dayjs": { + "optional": true + } + } + }, + "node_modules/@date-io/luxon": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.14.0.tgz", + "integrity": "sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "luxon": "^1.21.3 || ^2.x" + }, + "peerDependenciesMeta": { + "luxon": { + "optional": true + } + } + }, + "node_modules/@date-io/moment": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.14.0.tgz", + "integrity": "sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "moment": "^2.24.0" + }, + "peerDependenciesMeta": { + "moment": { + "optional": true + } + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2270,12 +2342,12 @@ } }, "node_modules/@emotion/cache": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz", - "integrity": "sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==", + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.9.3.tgz", + "integrity": "sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg==", "dependencies": { "@emotion/memoize": "^0.7.4", - "@emotion/sheet": "^1.1.0", + "@emotion/sheet": "^1.1.1", "@emotion/utils": "^1.0.0", "@emotion/weak-memoize": "^0.2.5", "stylis": "4.0.13" @@ -2408,15 +2480,14 @@ "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" }, "node_modules/@emotion/react": { - "version": "11.8.1", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.8.1.tgz", - "integrity": "sha512-XGaie4nRxmtP1BZYBXqC5JGqMYF2KRKKI7vjqNvQxyRpekVAZhb6QqrElmZCAYXH1L90lAelADSVZC4PFsrJ8Q==", + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.3.tgz", + "integrity": "sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ==", "dependencies": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", - "@emotion/cache": "^11.7.1", - "@emotion/serialize": "^1.0.2", - "@emotion/sheet": "^1.1.0", + "@emotion/cache": "^11.9.3", + "@emotion/serialize": "^1.0.4", "@emotion/utils": "^1.1.0", "@emotion/weak-memoize": "^0.2.5", "hoist-non-react-statics": "^3.3.1" @@ -2435,9 +2506,9 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", - "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.4.tgz", + "integrity": "sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg==", "dependencies": { "@emotion/hash": "^0.8.0", "@emotion/memoize": "^0.7.4", @@ -2447,9 +2518,9 @@ } }, "node_modules/@emotion/sheet": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", - "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.1.tgz", + "integrity": "sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==" }, "node_modules/@emotion/styled": { "version": "11.8.1", @@ -3743,6 +3814,61 @@ "react": "^17.0.0" } }, + "node_modules/@mui/x-date-pickers": { + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.0-alpha.6.tgz", + "integrity": "sha512-2JeagDwwa/V2XPj243cZg5ReZ2553OzukUAfbdxXwj9gGGLeXjBa95NP4kPOBOze4tJq1y/4aYt/aK50aZWElQ==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@date-io/date-fns": "^2.14.0", + "@date-io/dayjs": "^2.14.0", + "@date-io/luxon": "^2.14.0", + "@date-io/moment": "^2.14.0", + "@mui/utils": "^5.4.1", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "date-fns": "^2.25.0", + "dayjs": "^1.10.7", + "luxon": "^1.28.0 || ^2.0.0", + "moment": "^2.29.1", + "react": "^17.0.2 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -16504,6 +16630,18 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -27789,6 +27927,14 @@ "node": ">=0.10.0" } }, + "node_modules/rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -33865,6 +34011,43 @@ "postcss-value-parser": "^4.2.0" } }, + "@date-io/core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.14.0.tgz", + "integrity": "sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw==" + }, + "@date-io/date-fns": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/dayjs": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.14.0.tgz", + "integrity": "sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/luxon": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.14.0.tgz", + "integrity": "sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/moment": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.14.0.tgz", + "integrity": "sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -33920,12 +34103,12 @@ } }, "@emotion/cache": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz", - "integrity": "sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==", + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.9.3.tgz", + "integrity": "sha512-0dgkI/JKlCXa+lEXviaMtGBL0ynpx4osh7rjOXE71q9bIF8G+XhJgvi+wDu0B0IdCVx37BffiwXlN9I3UuzFvg==", "requires": { "@emotion/memoize": "^0.7.4", - "@emotion/sheet": "^1.1.0", + "@emotion/sheet": "^1.1.1", "@emotion/utils": "^1.0.0", "@emotion/weak-memoize": "^0.2.5", "stylis": "4.0.13" @@ -34059,24 +34242,23 @@ "integrity": "sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ==" }, "@emotion/react": { - "version": "11.8.1", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.8.1.tgz", - "integrity": "sha512-XGaie4nRxmtP1BZYBXqC5JGqMYF2KRKKI7vjqNvQxyRpekVAZhb6QqrElmZCAYXH1L90lAelADSVZC4PFsrJ8Q==", + "version": "11.9.3", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.9.3.tgz", + "integrity": "sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ==", "requires": { "@babel/runtime": "^7.13.10", "@emotion/babel-plugin": "^11.7.1", - "@emotion/cache": "^11.7.1", - "@emotion/serialize": "^1.0.2", - "@emotion/sheet": "^1.1.0", + "@emotion/cache": "^11.9.3", + "@emotion/serialize": "^1.0.4", "@emotion/utils": "^1.1.0", "@emotion/weak-memoize": "^0.2.5", "hoist-non-react-statics": "^3.3.1" } }, "@emotion/serialize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz", - "integrity": "sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.4.tgz", + "integrity": "sha512-1JHamSpH8PIfFwAMryO2bNka+y8+KA5yga5Ocf2d7ZEiJjb7xlLW7aknBGZqJLajuLOvJ+72vN+IBSwPlXD1Pg==", "requires": { "@emotion/hash": "^0.8.0", "@emotion/memoize": "^0.7.4", @@ -34086,9 +34268,9 @@ } }, "@emotion/sheet": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz", - "integrity": "sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.1.tgz", + "integrity": "sha512-J3YPccVRMiTZxYAY0IOq3kd+hUP8idY8Kz6B/Cyo+JuXq52Ek+zbPbSQUrVQp95aJ+lsAW7DPL1P2Z+U1jGkKA==" }, "@emotion/styled": { "version": "11.8.1", @@ -34986,6 +35168,23 @@ "react-is": "^17.0.2" } }, + "@mui/x-date-pickers": { + "version": "5.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.0-alpha.6.tgz", + "integrity": "sha512-2JeagDwwa/V2XPj243cZg5ReZ2553OzukUAfbdxXwj9gGGLeXjBa95NP4kPOBOze4tJq1y/4aYt/aK50aZWElQ==", + "requires": { + "@babel/runtime": "^7.17.2", + "@date-io/date-fns": "^2.14.0", + "@date-io/dayjs": "^2.14.0", + "@date-io/luxon": "^2.14.0", + "@date-io/moment": "^2.14.0", + "@mui/utils": "^5.4.1", + "clsx": "^1.1.1", + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -44938,6 +45137,11 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -53314,6 +53518,12 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "requires": {} + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index d31dcbce..a6dd7b1b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,18 +1,21 @@ { "name": "frontend", - "version": "1.2.0", + "version": "1.3.0", "private": true, "dependencies": { + "@date-io/date-fns": "^2.14.0", "@emotion/react": "^11.6.0", "@emotion/styled": "^11.6.0", "@mui/icons-material": "^5.4.4", "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", + "@mui/x-date-pickers": "^5.0.0-alpha.6", "@react-keycloak/web": "^3.4.0", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react": "^12.1.3", "@testing-library/user-event": "^13.5.0", "axios": "^0.26.0", + "date-fns": "^2.28.0", "jwt-decode": "^3.1.2", "keycloak-js": "^17.0.0", "prop-types": "^15.8.1", diff --git a/frontend/public/index.html b/frontend/public/index.html index 0a6828f3..3524c545 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,7 +7,7 @@ + diff --git a/frontend/public/tracker.js b/frontend/public/tracker.js new file mode 100644 index 00000000..51641efd --- /dev/null +++ b/frontend/public/tracker.js @@ -0,0 +1,40 @@ +if (window.itvr_config && window.itvr_config.REACT_APP_ENV === 'test') { + // + (function (p, l, o, w, i, n, g) { + if (!p[i]) { + p.GlobalSnowplowNamespace = p.GlobalSnowplowNamespace || []; + p.GlobalSnowplowNamespace.push(i); + p[i] = function () { + (p[i].q = p[i].q || []).push(arguments); + }; + p[i].q = p[i].q || []; + n = l.createElement(o); + g = l.getElementsByTagName(o)[0]; + n.async = 1; + n.src = w; + g.parentNode.insertBefore(n, g); + } + })( + window, + document, + 'script', + 'https://www2.gov.bc.ca/StaticWebResources/static/sp/sp-2-14-0.js', + 'snowplow' + ); + var collector = 'spm.apps.gov.bc.ca'; + window.snowplow('newTracker', 'rt', collector, { + appId: 'Snowplow_standalone', + cookieLifetime: 86400 * 548, + platform: 'web', + post: true, + forceSecureTracker: true, + contexts: { + webPage: true, + performanceTiming: true + } + }); + window.snowplow('enableActivityTracking', 30, 30); // Ping every 30 seconds after 30 seconds + window.snowplow('enableLinkClickTracking'); + window.snowplow('trackPageView'); + // +} diff --git a/frontend/src/components/BCEIDLogin.js b/frontend/src/components/BCEIDLogin.js new file mode 100644 index 00000000..41285374 --- /dev/null +++ b/frontend/src/components/BCEIDLogin.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { BCEID_KEYCLOAK_REALM } from '../config'; +import { useKeycloak } from '@react-keycloak/web'; +import { keycloakInitOptions, keycloaks } from '../keycloak'; +const BCEIDLogin = (props) => { + const { type = '', householdApplicationId = '' } = props; + const { keycloak } = useKeycloak(); + const redirectUri = householdApplicationId + ? `${window.location.origin}/householdForm?q=${householdApplicationId}` + : `${window.location.origin}/form`; + + return ( +
+

Basic BCeID Account

+ +
+
Alternate method to 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 {type === 'spouse' && ' or BC Services Card '} and a + secondary piece of ID.{' '} + + Learn more about ID requirements. + +
+
+ +
+ ); +}; + +export default BCEIDLogin; diff --git a/frontend/src/components/BottomBanner.js b/frontend/src/components/BottomBanner.js index 2235df0f..c1c66afe 100644 --- a/frontend/src/components/BottomBanner.js +++ b/frontend/src/components/BottomBanner.js @@ -55,42 +55,6 @@ const BottomBanner = (props) => { - -

Basic BCeID

- - -
- - Get a Basic BCeID account - -
-
diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js index fa54ea33..76777540 100644 --- a/frontend/src/components/Footer.js +++ b/frontend/src/components/Footer.js @@ -27,7 +27,9 @@ const Footer = () => {
  • - Contact Us + + Contact Us +
  • diff --git a/frontend/src/components/Form.js b/frontend/src/components/Form.js index 8971e525..c99609b3 100644 --- a/frontend/src/components/Form.js +++ b/frontend/src/components/Form.js @@ -26,6 +26,9 @@ import Loading from './Loading'; import { useKeycloak } from '@react-keycloak/web'; import BCSCInfo from './BCSCInfo'; import { addTokenFields } from '../keycloak'; +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; export const defaultValues = { sin: '', @@ -47,6 +50,8 @@ export const defaultValues = { const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { const [loading, setLoading] = useState(false); + const [DOB, setDOB] = useState(new Date()); + const [submitStatus, setSubmitStatus] = useState(true); const queryClient = useQueryClient(); const { keycloak } = useKeycloak(); const kcToken = keycloak.tokenParsed; @@ -82,6 +87,12 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { const onSubmit = (data) => { setNumberOfErrors(0); setLoading(true); + if (kcToken.identity_provider !== 'bcsc') { + data = { + ...data, + date_of_birth: data.date_of_birth.toISOString().slice(0, 10) + }; + } mutation.mutate(data, { onSuccess: (data, variables, context) => { const id = data.data.id; @@ -95,6 +106,15 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { }); }; + const checkDLStatus = (dl) => { + const detailUrl = `/api/application-form/check_status/?drivers_license=${dl}`; + axiosInstance.current.get(detailUrl).then((response) => { + response.data.drivers_license_valid === 'false' + ? setSubmitStatus(false) + : setSubmitStatus(true); + }); + }; + const onError = (errors) => { const numberOfErrors = Object.keys(errors).length; setNumberOfErrors(numberOfErrors); @@ -177,7 +197,7 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => {

    - Complete your household rebate application + Your application information

    secure form submission
    @@ -256,12 +276,20 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { name="date_of_birth" control={control} render={({ field }) => ( - setValue('date_of_birth', e.target.value)} - /> + + { + setValue('date_of_birth', newDate); + setDOB(newDate); + }} + renderInput={(params) => } + /> + )} rules={{ validate: (inputtedDOB) => { @@ -400,6 +428,12 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { {errors?.drivers_licence?.type === 'validate' && (

    Not a valid B.C. Driver's Licence Number

    )} + {!submitStatus && ( + + Error: This driver's licence number has already been submitted or + issued a rebate. + + )} BC Driver's Licence number (used for redeeming your rebate): @@ -416,6 +450,9 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { DL: } onChange={(e) => setValue('drivers_licence', e.target.value)} + onBlur={(e) => { + checkDLStatus(e.target.value); + }} /> )} rules={{ @@ -459,7 +496,7 @@ const Form = ({ setNumberOfErrors, setErrorsExistCounter }) => { paddingX: '30px', paddingY: '10px' }} - disabled={loading} + disabled={loading || !submitStatus} > Submit Application diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 68564484..9f549a5e 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -12,7 +12,13 @@ const Header = (props) => { > Government of B.C. - CleanBC Go Electric + + CleanBC Go Electric +

    Passenger vehicle rebates

    diff --git a/frontend/src/components/IndividualLogin.js b/frontend/src/components/IndividualLogin.js index a91c51e9..f2e1a214 100644 --- a/frontend/src/components/IndividualLogin.js +++ b/frontend/src/components/IndividualLogin.js @@ -2,7 +2,7 @@ import React from 'react'; import BottomBanner from './BottomBanner'; import Box from '@mui/material/Box'; import INeedHelp from './INeedHelp'; - +import BCEIDLogin from './BCEIDLogin'; const IndividualLogin = () => { return ( @@ -15,7 +15,7 @@ const IndividualLogin = () => {

    BC Driver’s Licence

    Used to connect you with your rebate.

    -

    BC Services Card app or Basic BCeID

    +

    BC Services Card app

    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. - -

    +

    Household applications

    For a household application your spouse or common law partner will @@ -43,9 +33,10 @@ const IndividualLogin = () => {

    + { formState: { errors }, setValue } = methods; + const [DOB, setDOB] = useState(new Date()); const axiosInstance = useAxios(); const queryFn = () => @@ -82,6 +86,12 @@ const SpouseForm = ({ id, setNumberOfErrors, setErrorsExistCounter }) => { const onSubmit = (data) => { setNumberOfErrors(0); setLoading(true); + if (kcToken.identity_provider !== 'bcsc') { + data = { + ...data, + date_of_birth: data.date_of_birth.toISOString().slice(0, 10) + }; + } mutation.mutate(data, { onSuccess: (data, variables, context) => { let refinedData = data.data; @@ -118,7 +128,7 @@ const SpouseForm = ({ id, setNumberOfErrors, setErrorsExistCounter }) => {
    -

    Apply for a passenger vehicle rebate

    +

    Apply for a passenger vehicle rebate pre-approval

    Complete your household rebate application @@ -221,12 +231,20 @@ const SpouseForm = ({ id, setNumberOfErrors, setErrorsExistCounter }) => { name="date_of_birth" control={control} render={({ field }) => ( - setValue('date_of_birth', e.target.value)} - sx={{ width: '300px' }} - /> + + { + setValue('date_of_birth', newDate); + setDOB(newDate); + }} + renderInput={(params) => } + /> + )} rules={{ validate: (inputtedDOB) => { diff --git a/frontend/src/components/upload/FileDropArea.js b/frontend/src/components/upload/FileDropArea.js index 55971564..9dd8d71b 100644 --- a/frontend/src/components/upload/FileDropArea.js +++ b/frontend/src/components/upload/FileDropArea.js @@ -47,8 +47,8 @@ const FileDropArea = ({ useEffect(() => { register(name, { validate: { - twoOrMore: (inputtedFiles) => { - if (!inputtedFiles || inputtedFiles.length < 2) { + exactlyTwo: (inputtedFiles) => { + if (!inputtedFiles || inputtedFiles.length !== 2) { return false; } return true; diff --git a/frontend/src/components/upload/Upload.js b/frontend/src/components/upload/Upload.js index c01d229e..24a937af 100644 --- a/frontend/src/components/upload/Upload.js +++ b/frontend/src/components/upload/Upload.js @@ -27,8 +27,8 @@ const Upload = (props) => { See examples of accepted ID - {errors?.documents?.type === 'validate' && ( -

    Need at least 2 files

    + {errors?.documents?.type === 'exactlyTwo' && ( +

    Need exactly 2 files

    )} {errors?.documents?.type === 'maxSize' && (

    No file may exceed 5MB

    diff --git a/frontend/src/pages/Form.js b/frontend/src/pages/Form.js index da0b786f..2a5a4de0 100644 --- a/frontend/src/pages/Form.js +++ b/frontend/src/pages/Form.js @@ -28,7 +28,7 @@ const FormPage = () => { Errors below, please ensure all fields are complete )} -

    Apply for a passenger vehicle rebate

    +

    Apply for a passenger vehicle rebate pre-approval

    { const [searchParams] = useSearchParams(); const householdApplicationId = searchParams.get('q'); @@ -19,7 +19,7 @@ const HouseholdLogin = () => { Used to confirm your income. To give consent to the Canada Revenue Agency (CRA) to disclose your income information.

    -

    BC Services Card app or Basic BCeID

    +

    BC Services Card app

    Used to confirm your identity.

    The{' '} @@ -28,17 +28,6 @@ const HouseholdLogin = () => { 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 or BC Services Card and a secondary piece of ID.{' '} - - Learn more about ID requirements. - -

    ); @@ -55,6 +44,10 @@ const HouseholdLogin = () => { type="spouse" householdApplicationId={householdApplicationId} /> + @@ -15,16 +12,6 @@ function Index() { - {keycloak.authenticated && ( - - )} ); } diff --git a/frontend/src/stories/HouseholdLogin.stories.jsx b/frontend/src/stories/HouseholdLogin.stories.jsx deleted file mode 100644 index 60fc1b8a..00000000 --- a/frontend/src/stories/HouseholdLogin.stories.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; - -import HouseholdLogin from '../pages/HouseholdLogin'; - -export default { - title: 'ITVR/HouseholdLogin', - component: HouseholdLogin -}; - -const Template = (args) => ; -export const Default = Template.bind({}); diff --git a/frontend/src/stories/Login.stories.jsx b/frontend/src/stories/Login.stories.jsx new file mode 100644 index 00000000..b7fcaf6b --- /dev/null +++ b/frontend/src/stories/Login.stories.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import HouseholdLogin from '../pages/HouseholdLogin'; +import IndividualLogin from '../components/IndividualLogin'; + +export default { + title: 'ITVR/Login', + component: IndividualLogin +}; + +const TemplateIndividual = (args) => ; +export const Individual = TemplateIndividual.bind({}); + +const TemplateHousehold = (args) => ; +export const Household = TemplateHousehold.bind({}); diff --git a/frontend/src/styles/Footer.scss b/frontend/src/styles/Footer.scss index d09538ab..190b4f70 100644 --- a/frontend/src/styles/Footer.scss +++ b/frontend/src/styles/Footer.scss @@ -3,43 +3,61 @@ footer { border-top: 2px solid #fcba19; color: #fff; font-family: ‘BCSans’, ‘Noto Sans’, Verdana, Arial, sans-serif; + .container { + display: flex; + justify-content: center; + flex-direction: column; + text-align: center; + height: 46px; + } + ul { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: 0; + color: #fff; + list-style: none; + align-items: center; + height: 100%; + li a { + font-size: 0.813em; + font-weight: normal; /* 400 */ + color: #fff; + border-right: 1px solid #4b5e7e; + padding-left: 5px; + padding-right: 5px; + text-decoration: none; + } + a:hover { + color: #fff; + text-decoration: underline; + } + a:focus { + outline: 4px solid #3b99fc; + outline-offset: 1px; + } + } } - -footer .container { - display: flex; - justify-content: center; - flex-direction: column; - text-align: center; - height: 46px; +@media (max-width: 600px) { + .footer { + .container { + ul li a { + font-size: 0.8rem; + line-height: 0.9rem; + } + } + } } - -footer ul { - display: flex; - flex-direction: row; - flex-wrap: wrap; - margin: 0; - color: #fff; - list-style: none; - align-items: center; - height: 100%; -} - -footer ul li a { - font-size: 0.813em; - font-weight: normal; /* 400 */ - color: #fff; - border-right: 1px solid #4b5e7e; - padding-left: 5px; - padding-right: 5px; - text-decoration: none; -} - -footer a:hover { - color: #fff; - text-decoration: underline; +@media (max-width: 485px) { + .footer { + height: 3.5rem; + .container { + justify-content: space-evenly; + } + } } - -footer a:focus { - outline: 4px solid #3b99fc; - outline-offset: 1px; +@media (max-width: 282px) { + .footer { + height: 6rem; + } } diff --git a/frontend/src/styles/Header.scss b/frontend/src/styles/Header.scss index a590568c..31c30a41 100644 --- a/frontend/src/styles/Header.scss +++ b/frontend/src/styles/Header.scss @@ -1,13 +1,16 @@ .cleanbc-banner { background-color: $banner-blue; font-size: 1.25rem; - color: $white; - border-bottom: 3px solid $border-orange; display: flex; + border-bottom: 3px solid $border-orange; flex-direction: row; justify-content: flex-start; align-items: center; - + a { + color: $white; + text-decoration: none; + font-weight: bold; + } img { margin-left: 10rem; height: 4.5rem; @@ -23,8 +26,6 @@ .title { h1 { color: $white; - margin-top: 1.5rem; - margin-bottom: 1.5rem; } h1 { padding-left: 6rem; @@ -68,7 +69,7 @@ } @media (max-width: 375px) { .page-header { - span { + a { font-size: 10pt; } .title { diff --git a/frontend/src/styles/Login.scss b/frontend/src/styles/Login.scss index 7ff453cd..7bb298ce 100644 --- a/frontend/src/styles/Login.scss +++ b/frontend/src/styles/Login.scss @@ -1,3 +1,6 @@ +#bceid-login-button { + margin: 1.5rem 0 0 0; +} .login-square { display: inline-block; background-color: $background-light-blue; @@ -10,7 +13,6 @@ .whats-needed { &-individual, &-spouse { - padding: 2rem 8rem 2rem 8rem; margin-top: 2rem; } &-individual { @@ -50,7 +52,6 @@ margin-top: 2rem; } } - .start-application { &-individual, &-spouse { @@ -69,18 +70,19 @@ } } .login-square { - height: 9rem; + height: 10rem; width: 17rem; margin-right: auto; padding: 1rem; h2 { margin: 2px !important; + font-size: 1.5rem; } .button { font-size: 1rem; margin-top: 0.5rem; - margin-bottom: 0; + margin-bottom: 1rem; } } } diff --git a/frontend/src/utility.js b/frontend/src/utility.js index cf3ec40d..5b43dd61 100644 --- a/frontend/src/utility.js +++ b/frontend/src/utility.js @@ -32,6 +32,7 @@ export const isAgeValid = (dob, lowerBound, upperBound) => { if (!dob) { return false; } + dob = dob.toISOString().slice(0,10) const dobSplit = dob.split('-'); const dobYear = parseInt(dobSplit[0]); const dobMonthIndex = parseInt(dobSplit[1]) - 1; diff --git a/openshift/templates/backend/README.md b/openshift/templates/backend/README.md index 17df1a42..0040ba7c 100644 --- a/openshift/templates/backend/README.md +++ b/openshift/templates/backend/README.md @@ -12,7 +12,7 @@ 2. Create template secret template.django-secret, template.django-salt -3. create secret itvr-patroni-app, itvr-email-service, itvr-object-storage and itvr-superuser(prod only) +3. create secret itvr-patroni-app, itvr-email-service, itvr-object-storage, itvr-ncda and itvr-superuser(prod only) 4. create user for itvr database, create user [username] with password '[password]' diff --git a/openshift/templates/backend/backend-dc.yaml b/openshift/templates/backend/backend-dc.yaml index cf4436dc..8c7a2b2d 100644 --- a/openshift/templates/backend/backend-dc.yaml +++ b/openshift/templates/backend/backend-dc.yaml @@ -347,6 +347,31 @@ objects: key: root-password - name: MINIO_BUCKET_NAME value: ${BUCKET_NAME} + - name: NCDA_CLIENT_ID + valueFrom: + secretKeyRef: + name: itvr-ncda + key: NCDA_CLIENT_ID + - name: NCDA_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: itvr-ncda + key: NCDA_CLIENT_SECRET + - name: NCDA_RESOURCE + valueFrom: + secretKeyRef: + name: itvr-ncda + key: NCDA_RESOURCE + - name: NCDA_AUTH_URL + valueFrom: + secretKeyRef: + name: itvr-ncda + key: NCDA_AUTH_URL + - name: NCDA_SHAREPOINT_URL + valueFrom: + secretKeyRef: + name: itvr-ncda + key: NCDA_SHAREPOINT_URL - name: DJANGO_DEBUG value: ${DJANGO_DEBUG} livenessProbe: diff --git a/openshift/templates/backend/itvr-ncda-secret.yaml b/openshift/templates/backend/itvr-ncda-secret.yaml new file mode 100644 index 00000000..09465ab0 --- /dev/null +++ b/openshift/templates/backend/itvr-ncda-secret.yaml @@ -0,0 +1,25 @@ +apiVersion: template.openshift.io/v1 +kind: Template +parameters: +- name: NCDA_CLIENT_ID + required: true +- name: NCDA_CLIENT_SECRET + required: true +- name: NCDA_RESOURCE + required: true +- name: NCDA_AUTH_URL + required: true +- name: NCDA_SHAREPOINT_URL + required: true +objects: +- apiVersion: v1 + kind: Secret + metadata: + annotations: null + name: itvr-ncda + stringData: + NCDA_CLIENT_ID: ${NCDA_CLIENT_ID} + NCDA_CLIENT_SECRET: ${NCDA_CLIENT_SECRET} + NCDA_RESOURCE: ${NCDA_RESOURCE} + NCDA_AUTH_URL: ${NCDA_AUTH_URL} + NCDA_SHAREPOINT_URL: ${NCDA_SHAREPOINT_URL} diff --git a/openshift/templates/clamav/Dockerfile b/openshift/templates/clamav/Dockerfile new file mode 100644 index 00000000..43d1d6bd --- /dev/null +++ b/openshift/templates/clamav/Dockerfile @@ -0,0 +1,18 @@ +FROM artifacts.developer.gov.bc.ca/docker-remote/clamav/clamav:0.105_base +RUN mkdir /run/clamav && \ + chown clamav:clamav /run/clamav && \ + chmod 750 /run/clamav +# && \ +# chown clamav:clamav /var/lib/clamav && \ +# chmod 750 /var/lib/clamav + +#RUN chgrp -R root /var/log/clamav && \ +# chmod -R g+w /var/log/clamav && \ +# chgrp -R root /var/lib/clamav && \ +# chmod -R g+w /var/lib/clamav && \ +# chgrp -R root /run/clamav && \ +# chmod -R g+w /run/clamav + + +#chown: /var/lib/clamav: Operation not permitted +#chown: /var/lib/clamav: Operation not permitted \ No newline at end of file diff --git a/openshift/templates/clamav/clamav-bc.yaml b/openshift/templates/clamav/clamav-bc.yaml new file mode 100644 index 00000000..01d66f68 --- /dev/null +++ b/openshift/templates/clamav/clamav-bc.yaml @@ -0,0 +1,73 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + creationTimestamp: null + name: frontend +parameters: + - name: GIT_URL + displayName: + description: itvr repo + value: https://github.com/bcgov/itvr.git + required: true + - name: GIT_REF + displayName: + description: itvr branch name of the pr + value: clamav-1.3.0 + required: true +objects: +- apiVersion: image.openshift.io/v1 + kind: ImageStream + metadata: + annotations: + description: Keeps track of changes in the clamav image + labels: + shared: "true" + creationTimestamp: null + name: itvr-clamav + spec: + lookupPolicy: + local: false + status: + dockerImageRepository: "" +- apiVersion: build.openshift.io/v1 + kind: BuildConfig + metadata: + name: itvr-clamav + creationTimestamp: + annotations: + description: Defines how to build the clamav image in docker + spec: + output: + to: + kind: ImageStreamTag + name: itvr-clamav:0.105_base + resources: + limits: + cpu: 1500m + memory: 1300Mi + requests: + cpu: 750m + memory: 650Mi + source: + contextDir: openshift/templates/clamav + git: + uri: ${GIT_URL} + ref: ${GIT_REF} + type: Git + strategy: + dockerStrategy: + env: + - name: ARTIFACTORY_USER + valueFrom: + secretKeyRef: + name: artifacts-default-pwpgbz + key: username + - name: ARTIFACTORY_PASSWORD + valueFrom: + secretKeyRef: + name: artifacts-default-pwpgbz + key: password + type: Docker + triggers: + - type: ConfigChange + - type: ImageChange \ No newline at end of file diff --git a/openshift/templates/clamav/clamav-dc.yaml b/openshift/templates/clamav/clamav-dc.yaml new file mode 100644 index 00000000..e3eeee81 --- /dev/null +++ b/openshift/templates/clamav/clamav-dc.yaml @@ -0,0 +1,135 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + creationTimestamp: null + name: frontend-dc +objects: + - kind: PersistentVolumeClaim + apiVersion: v1 + metadata: + name: clamav-signature-db + annotations: + volume.beta.kubernetes.io/storage-class: netapp-file-standard + template.openshift.io.bcgov/create: 'true' + spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi + status: {} + - kind: Service + apiVersion: v1 + metadata: + name: itvr-clamav + creationTimestamp: + labels: + name: tfrs-clamav + annotations: + openshift.io/generated-by: OpenShiftWebConsole + spec: + ports: + - name: 3310-tcp + protocol: TCP + port: 3310 + targetPort: 3310 + selector: + name: tfrs-clamav + type: ClusterIP + sessionAffinity: None + status: + loadBalancer: {} + - apiVersion: apps.openshift.io/v1 + kind: DeploymentConfig + metadata: + name: itvr-clamav + annotations: + description: Defines how to deploy the clamav application + creationTimestamp: null + spec: + replicas: 1 + revisionHistoryLimit: 10 + automountServiceAccountToken: false + selector: + name: itvr-clamav + strategy: + activeDeadlineSeconds: 21600 + recreateParams: + timeoutSeconds: 600 + resources: {} + type: Recreate + template: + metadata: + creationTimestamp: null + labels: + name: itvr-clamav + spec: + volumes: + - name: clamav-signature-db + persistentVolumeClaim: + claimName: clamav-signature-db + containers: + - name: itvr-clamav + env: null + image: + imagePullPolicy: IfNotPresent + volumeMounts: + - name: clamav-signature-db + mountPath: /var/lib/clamav + defaultMode: 666 + livenessProbe: + tcpSocket: + port: 3310 + initialDelaySeconds: 60 + timeoutSeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: 3310 + initialDelaySeconds: 60 + timeoutSeconds: 3 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + ports: + - containerPort: 3310 + protocol: TCP + resources: + limits: + cpu: 500m + memory: 4Gi + requests: + cpu: 250m + memory: 2Gi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + runAsUser: 1008280000 + runAsGroup: 1008280000 + fsGroup: 1008280000 + terminationGracePeriodSeconds: 30 + test: false + triggers: + - imageChangeParams: + automatic: true + containerNames: + - itvr-clamav + from: + kind: ImageStreamTag + name: itvr-clamav:0.105_base + namespace: ac294c-tools + lastTriggeredImage: + type: ImageChange + - type: ConfigChange + status: + availableReplicas: 0 + latestVersion: 0 + observedGeneration: 0 + replicas: 0 + unavailableReplicas: 0 + updatedReplicas: 0 diff --git a/openshift/templates/frontend/frontend-dc-docker.yaml b/openshift/templates/frontend/frontend-dc-docker.yaml index 2c1dd722..410f19d8 100644 --- a/openshift/templates/frontend/frontend-dc-docker.yaml +++ b/openshift/templates/frontend/frontend-dc-docker.yaml @@ -14,7 +14,7 @@ parameters: required: true - name: VERSION displayName: - description: image tag name for output + description: image tag name for output required: true - name: ENV_NAME value: dev @@ -42,7 +42,7 @@ parameters: description: Memory upper limit required: true - name: REPLICAS - value: '1' + value: "1" required: true - name: REACT_APP_BCSC_KEYCLOAK_CLIENT_ID displayName: REACT_APP_BCSC_KEYCLOAK_CLIENT_ID @@ -55,7 +55,7 @@ parameters: - name: REACT_APP_BCSC_KEYCLOAK_URL displayName: REACT_APP_BCSC_KEYCLOAK_URL description: keycload url for BC Service Card - required: true + required: true - name: REACT_APP_BCEID_KEYCLOAK_CLIENT_ID displayName: REACT_APP_BCEID_KEYCLOAK_CLIENT_ID description: keycload client id for BCeID @@ -101,7 +101,8 @@ objects: "REACT_APP_BCEID_KEYCLOAK_CLIENT_ID": "${REACT_APP_BCEID_KEYCLOAK_CLIENT_ID}", "REACT_APP_BCEID_KEYCLOAK_REALM": "${REACT_APP_BCEID_KEYCLOAK_REALM}", "REACT_APP_BCEID_KEYCLOAK_URL": "${REACT_APP_BCEID_KEYCLOAK_URL}", - "REACT_APP_API_BASE": "${REACT_APP_API_BASE}" + "REACT_APP_API_BASE": "${REACT_APP_API_BASE}", + "REACT_APP_ENV": "${ENV_NAME}" }; - apiVersion: v1 kind: Service @@ -129,8 +130,8 @@ objects: kind: Route metadata: creationTimestamp: null - annotations: - haproxy.router.openshift.io/timeout: 1200s + annotations: + haproxy.router.openshift.io/timeout: 1200s labels: name: frontend app: zeva diff --git a/openshift/templates/task-queue/task-queue-dc.yaml b/openshift/templates/task-queue/task-queue-dc.yaml index 96501763..4cb3b44c 100644 --- a/openshift/templates/task-queue/task-queue-dc.yaml +++ b/openshift/templates/task-queue/task-queue-dc.yaml @@ -198,6 +198,8 @@ objects: value: ${DJANGO_DEBUG} - name: DJANGO_TASKS value: 'true' + - name: CORS_ORIGIN_WHITELIST + value: "https://${NAME}${SUFFIX}.apps.silver.devops.gov.bc.ca" readinessProbe: exec: command: diff --git a/tests/perf/testfrontend.js b/tests/perf/testfrontend.js new file mode 100644 index 00000000..a717dacd --- /dev/null +++ b/tests/perf/testfrontend.js @@ -0,0 +1,15 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + stages: [ + { duration: '2ms', target: 20 }, + { duration: '20s', target: 0 }, + ], +}; + +export default function () { + const res = http.get('https://itvr-dev-148.apps.silver.devops.gov.bc.ca/'); + //check(res, { 'status was 200': (r) => r.status == 200 }); + sleep(1); +} \ No newline at end of file