From 16cb7dffb6c6d0f486ba1a3122551fe0f5502c2a Mon Sep 17 00:00:00 2001
From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com>
Date: Thu, 10 Dec 2020 13:59:43 -0800
Subject: [PATCH] Tracking pull request to merge release-1.15.0.1 to master
(#424)
Fix site slowness issue, update configmap mount location on frontend and correct prod logoff url
---
.pipeline-v3/lib/config.js | 6 +-
.pipeline-v3/lib/deploy.js | 3 +-
.pipeline/lib/config.js | 4 +-
.yo-rc.json | 16 +-
.yo-rc.json-v4 | 54 -------
backend/api/serializers/credit_transfer.py | 7 +-
backend/api/serializers/sales_submission.py | 111 +++++++++----
backend/api/services/credit_transaction.py | 26 ++-
backend/api/services/sales_spreadsheet.py | 8 +-
backend/api/tests/test_credit_transfers.py | 98 +++++++++++-
backend/api/tests/test_vehicles.py | 21 +++
backend/api/viewsets/credit_request.py | 123 ++++++++++++++-
frontend/package.json | 2 +-
frontend/src/app/routes/CreditRequests.js | 2 +
.../credits/CreditRequestVINListContainer.js | 52 +++---
.../credits/UploadCreditRequestContainer.js | 9 ++
.../components/CreditRequestDetailsPage.js | 8 +-
.../components/CreditRequestListTable.js | 4 +-
.../components/CreditRequestVINListPage.js | 21 ++-
.../components/CreditTransfersDetailsPage.js | 10 +-
.../credits/components/SubmissionListTable.js | 149 ------------------
.../src/credits/components/VINListTable.js | 78 +++++++--
openshift-v3/templates/backend/README.md | 3 +
.../templates/backend/backend-dc.yaml | 30 ++--
.../backend/email-service-secret.yaml | 26 +++
openshift-v3/templates/config/configmap.yaml | 10 +-
.../templates/frontend/frontend-dc.yaml | 2 +-
openshift-v3/templates/nagios/nagios-dc.yaml | 2 -
openshift-v3/templates/patroni/README.md | 2 +-
.../unittest/backend-dc-unittest.yaml | 10 --
openshift/templates/backend/README.md | 3 +
openshift/templates/backend/backend-dc.yaml | 30 ++--
.../backend/email-service-secret.yaml | 26 +++
openshift/templates/config/configmap.yaml | 4 +-
openshift/templates/frontend/frontend-dc.yaml | 2 +-
openshift/templates/nagios/nagios-dc.yaml | 2 -
.../commands/check_email_connection.sh | 19 ++-
openshift/templates/patroni/README.md | 2 +-
.../unittest/backend-dc-unittest.yaml | 10 --
39 files changed, 612 insertions(+), 383 deletions(-)
delete mode 100644 .yo-rc.json-v4
delete mode 100644 frontend/src/credits/components/SubmissionListTable.js
create mode 100644 openshift-v3/templates/backend/email-service-secret.yaml
create mode 100644 openshift/templates/backend/email-service-secret.yaml
diff --git a/.pipeline-v3/lib/config.js b/.pipeline-v3/lib/config.js
index 627408f65..17c863624 100644
--- a/.pipeline-v3/lib/config.js
+++ b/.pipeline-v3/lib/config.js
@@ -14,7 +14,7 @@ const phases = {
dev: {namespace:'tbiwaq-dev', transient:true, name: `${name}`, ssoSuffix:'-dev',
ssoName:'dev.oidc.gov.bc.ca', phase: 'dev' , changeId:`${changeId}`, suffix: `-dev-${changeId}`,
instance: `${name}-dev-${changeId}` , version:`${version}-${changeId}`, tag:`dev-${version}-${changeId}`,
- host: `zeva-dev-${changeId}.${ocpName}.gov.bc.ca`, djangoDebug: 'True',
+ host: `zeva-dev-${changeId}.${ocpName}.gov.bc.ca`, djangoDebug: 'True', logoutHost: 'logontest.gov.bc.ca',
frontendCpuRequest: '100m', frontendCpuLimit: '700m', frontendMemoryRequest: '300M', frontendMemoryLimit: '4G', frontendReplicas: 1,
backendCpuRequest: '300m', backendCpuLimit: '600m', backendMemoryRequest: '1G', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendHost: `zeva-backend-dev-${changeId}.${ocpName}.gov.bc.ca`, backendReplicas: 1,
minioCpuRequest: '100m', minioCpuLimit: '200m', minioMemoryRequest: '200M', minioMemoryLimit: '500M', minioPvcSize: '1G',
@@ -25,7 +25,7 @@ const phases = {
test: {namespace:'tbiwaq-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: `zeva-test.${ocpName}.gov.bc.ca`, djangoDebug: 'False',
+ host: `zeva-test.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHost: 'logontest.gov.bc.ca',
frontendCpuRequest: '300m', frontendCpuLimit: '600m', frontendMemoryRequest: '500M', frontendMemoryLimit: '1G', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5,
backendCpuRequest: '100m', backendCpuLimit: '500m', backendMemoryRequest: '500M', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendReplicas: 1, backendMinReplicas: 2, backendMaxReplicas: 5, backendHost: `zeva-backend-test.${ocpName}.gov.bc.ca`,
minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '5G',
@@ -36,7 +36,7 @@ const phases = {
prod: {namespace:'tbiwaq-prod', name: `${name}`, ssoSuffix:'',
ssoName:'oidc.gov.bc.ca', phase: 'prod' , changeId:`${changeId}`, suffix: `-prod`,
instance: `${name}-prod`, version:`${version}`, tag:`prod-${version}`,
- host: `zeroemissionvehicles.${ocpName}.gov.bc.ca`, djangoDebug: 'False',
+ host: `zeroemissionvehicles.${ocpName}.gov.bc.ca`, djangoDebug: 'False', logoutHost: 'logon7.gov.bc.ca',
frontendCpuRequest: '300m', frontendCpuLimit: '600m', frontendMemoryRequest: '1G', frontendMemoryLimit: '2G', frontendReplicas: 2, frontendMinReplicas: 2, frontendMaxReplicas: 5,
backendCpuRequest: '200m', backendCpuLimit: '700m', backendMemoryRequest: '1G', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendReplicas: 1, backendMinReplicas: 2, backendMaxReplicas: 5, backendHost: `zeva-backend-prod.${ocpName}.gov.bc.ca`,
minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '10G',
diff --git a/.pipeline-v3/lib/deploy.js b/.pipeline-v3/lib/deploy.js
index f268b1f2e..9c1445536 100755
--- a/.pipeline-v3/lib/deploy.js
+++ b/.pipeline-v3/lib/deploy.js
@@ -32,7 +32,8 @@ module.exports = settings => {
'SSO_NAME': phases[phase].ssoName,
'KEYCLOAK_REALM': 'rzh2zkjq',
'DJANGO_DEBUG': phases[phase].djangoDebug,
- 'OCP_NAME': phases[phase].ocpName
+ 'OCP_NAME': phases[phase].ocpName,
+ 'LOGOUT_HOST': phases[phase].logoutHost
}
}))
diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js
index b2016ef05..979a29e60 100644
--- a/.pipeline/lib/config.js
+++ b/.pipeline/lib/config.js
@@ -31,7 +31,7 @@ const phases = {
minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '5G',
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: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '500M', patroniMemoryLimit: '1G', patroniPvcSize: '40G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`},
+ patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '500M', patroniMemoryLimit: '1G', patroniPvcSize: '5G', patroniReplica: 2, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`},
prod: {namespace:'e52f12-prod', name: `${name}`, ssoSuffix:'',
ssoName:'oidc.gov.bc.ca', phase: 'prod' , changeId:`${changeId}`, suffix: `-prod`,
@@ -42,7 +42,7 @@ const phases = {
minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '10G',
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: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '1G', patroniMemoryLimit: '2G', patroniPvcSize: '50G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`},
+ patroniCpuRequest: '500m', patroniCpuLimit: '1000m', patroniMemoryRequest: '1G', patroniMemoryLimit: '2G', patroniPvcSize: '30G', patroniReplica: 3, storageClass: 'netapp-block-standard', ocpName: `${ocpName}`},
};
diff --git a/.yo-rc.json b/.yo-rc.json
index b592ba595..6c663d39e 100644
--- a/.yo-rc.json
+++ b/.yo-rc.json
@@ -8,16 +8,16 @@
"path": ".",
"environments": {
"build": {
- "namespace": "tbiwaq-tools"
+ "namespace": "f52f12-tools"
},
"dev": {
- "namespace": "tbiwaq-dev"
+ "namespace": "f52f12-dev"
},
"test": {
- "namespace": "tbiwaq-test"
+ "namespace": "f52f12-test"
},
"prod": {
- "namespace": "tbiwaq-prod"
+ "namespace": "f52f12-prod"
}
},
"jenkinsJobName": "zeva",
@@ -29,17 +29,17 @@
"jenkins": {
"path": ".jenkins",
"name": "jenkins",
- "namespace": "tbiwaq-tools",
+ "namespace": "f52f12-tools",
"version": "1.0.0",
"environments": {
"build": {
- "namespace": "tbiwaq-tools"
+ "namespace": "f52f12-tools"
},
"dev": {
- "namespace": "tbiwaq-tools"
+ "namespace": "f52f12-tools"
},
"prod": {
- "namespace": "tbiwaq-tools"
+ "namespace": "f52f12-tools"
}
},
"jenkinsJobName": "_jenkins",
diff --git a/.yo-rc.json-v4 b/.yo-rc.json-v4
deleted file mode 100644
index 6c663d39e..000000000
--- a/.yo-rc.json-v4
+++ /dev/null
@@ -1,54 +0,0 @@
-{
- "@bcgov/bcdk": {
- "promptValues": {
- "modules": {
- "zeva": {
- "name": "zeva",
- "version": "1.0.0",
- "path": ".",
- "environments": {
- "build": {
- "namespace": "f52f12-tools"
- },
- "dev": {
- "namespace": "f52f12-dev"
- },
- "test": {
- "namespace": "f52f12-test"
- },
- "prod": {
- "namespace": "f52f12-prod"
- }
- },
- "jenkinsJobName": "zeva",
- "github_owner": "bcgov",
- "github_repo": "zeva",
- "jenkinsFilePath": "Jenkinsfile",
- "uuid": "2c0c5c5b-6d52-4ae1-aefb-00b5bbd7c7e3"
- },
- "jenkins": {
- "path": ".jenkins",
- "name": "jenkins",
- "namespace": "f52f12-tools",
- "version": "1.0.0",
- "environments": {
- "build": {
- "namespace": "f52f12-tools"
- },
- "dev": {
- "namespace": "f52f12-tools"
- },
- "prod": {
- "namespace": "f52f12-tools"
- }
- },
- "jenkinsJobName": "_jenkins",
- "github_owner": "bcgov",
- "github_repo": "zeva",
- "jenkinsFilePath": ".jenkins/Jenkinsfile",
- "uuid": "7a55c783-db16-45ad-82cc-23792286d6c3"
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/backend/api/serializers/credit_transfer.py b/backend/api/serializers/credit_transfer.py
index 8ec935d8d..54a39d3c4 100644
--- a/backend/api/serializers/credit_transfer.py
+++ b/backend/api/serializers/credit_transfer.py
@@ -208,7 +208,12 @@ def create(self, validated_data):
signing_authority_assertion_id=confirmation
)
- serializer = CreditTransferSerializer(credit_transfer, read_only=True)
+ serializer = CreditTransferSerializer(
+ credit_transfer, read_only=True,
+ context={
+ 'request': request
+ }
+ )
return serializer.data
diff --git a/backend/api/serializers/sales_submission.py b/backend/api/serializers/sales_submission.py
index 0e28f0dc6..b9e231877 100644
--- a/backend/api/serializers/sales_submission.py
+++ b/backend/api/serializers/sales_submission.py
@@ -87,6 +87,27 @@ class SalesSubmissionListSerializer(
validation_status = SerializerMethodField()
submission_history = SerializerMethodField()
+ def get_submission_history(self, obj):
+ request = self.context.get('request')
+ if not request.user.is_government and obj.validation_status in [
+ SalesSubmissionStatuses.RECOMMEND_REJECTION,
+ SalesSubmissionStatuses.RECOMMEND_APPROVAL,
+ SalesSubmissionStatuses.CHECKED,
+ ]:
+ # return the date that it was submitted
+ history = SalesSubmissionHistory.objects.filter(
+ submission_id=obj.id,
+ validation_status=SalesSubmissionStatuses.SUBMITTED
+ ).order_by('update_timestamp').first()
+ return history.update_timestamp.date()
+ # return the last updated date
+ history = SalesSubmissionHistory.objects.filter(
+ submission_id=obj.id,
+ validation_status=obj.validation_status
+ ).order_by('update_timestamp').first()
+ return history.update_timestamp.date()
+
+
def get_submission_history(self, obj):
request = self.context.get('request')
if not request.user.is_government and obj.validation_status in [
@@ -173,9 +194,7 @@ def get_total_warnings(self, obj):
]
if obj.validation_status in valid_statuses:
- for row in obj.content.all():
- if len(row.warnings) > 0 and row.record_of_sale is None:
- warnings += 1
+ warnings = obj.unselected
return warnings
@@ -192,22 +211,6 @@ class Meta:
'total_credits', 'total_warnings', 'unselected',
)
-
-class SalesSubmissionHistorySerializer(
- ModelSerializer, EnumSupportSerializerMixin, BaseSerializer
-):
- create_user = SerializerMethodField()
- update_user = SerializerMethodField()
- validation_status = SerializerMethodField()
-
- class Meta:
- model = SalesSubmissionHistory
- fields = (
- 'create_timestamp', 'create_user',
- 'validation_status', 'update_user'
- )
-
-
class SalesSubmissionSerializer(
ModelSerializer, EnumSupportSerializerMixin,
BaseSerializer
@@ -353,6 +356,31 @@ class Meta:
)
+class SalesSubmissionDetailSerializer(
+ ModelSerializer, EnumSupportSerializerMixin,
+ BaseSerializer
+):
+ content = SerializerMethodField()
+
+ def get_content(self, instance):
+ request = self.context.get('request')
+
+ serializer = SalesSubmissionContentSerializer(
+ instance.content,
+ read_only=True,
+ many=True,
+ context={'request': request}
+ )
+
+ return serializer.data
+
+ class Meta:
+ model = SalesSubmission
+ fields = (
+ 'id', 'validation_status', 'content', 'submission_id'
+ )
+
+
class SalesSubmissionSaveSerializer(
ModelSerializer
):
@@ -372,7 +400,7 @@ def validate_validation_status(self, value):
def update(self, instance, validated_data):
request = self.context.get('request')
- records = request.data.get('records')
+ invalidated = request.data.get('invalidated', None)
sales_submission_comment = validated_data.pop('sales_submission_comment', None)
if sales_submission_comment:
@@ -382,15 +410,44 @@ def update(self, instance, validated_data):
comment=sales_submission_comment.get('comment')
)
- if records is not None:
+ if invalidated is not None:
RecordOfSale.objects.filter(submission_id=instance.id).delete()
+ valid_vehicles = Vehicle.objects.filter(
+ organization_id=instance.organization_id,
+ validation_status=VehicleDefinitionStatuses.VALIDATED
+ ).values_list('model_year__name', Upper('make'), 'model_name')
+
+ duplicate_vins = SalesSubmissionContent.objects.annotate(
+ vin_count=Count('xls_vin')
+ ).filter(
+ submission_id=instance.id,
+ vin_count__gt=1
+ ).values_list('xls_vin', flat=True)
+
+ awarded_vins = RecordOfSale.objects.exclude(
+ submission_id=instance.id
+ ).values_list('vin', flat=True)
+
+ content = SalesSubmissionContent.objects.filter(
+ submission_id=instance.id
+ ).exclude(
+ id__in=invalidated
+ ).exclude(
+ xls_vin__in=awarded_vins
+ ).exclude(
+ xls_vin__in=duplicate_vins,
+ xls_sale_date__lte="43102.0"
+ )
- for record_id in records:
- row = SalesSubmissionContent.objects.filter(
- id=record_id
- ).first()
+ for row in content:
+ try:
+ model_year = int(float(row.xls_model_year))
+ except ValueError:
+ model_year = 0
- if row and row.vehicle:
+ if (
+ str(model_year), row.xls_make.upper(), row.xls_model,
+ ) in valid_vehicles:
RecordOfSale.objects.create(
sale_date=get_date(
row.xls_sale_date,
@@ -408,7 +465,7 @@ def update(self, instance, validated_data):
if validation_status:
SalesSubmissionHistory.objects.create(
- submission=instance,
+ submission_id=instance.id,
validation_status=validation_status,
update_user=request.user.username,
create_user=request.user.username,
diff --git a/backend/api/services/credit_transaction.py b/backend/api/services/credit_transaction.py
index e24a5b92d..47690f583 100644
--- a/backend/api/services/credit_transaction.py
+++ b/backend/api/services/credit_transaction.py
@@ -13,6 +13,8 @@
from api.models.vehicle import Vehicle
from api.models.weight_class import WeightClass
from api.services.credit_transfer import aggregate_credit_transfer_details
+from django.core.exceptions import ValidationError
+
def award_credits(submission):
records = RecordOfSale.objects.filter(
@@ -182,19 +184,17 @@ def validate_transfer(transfer):
weight_type = each.weight_class.id
# check if supplier has enough for this transfer
for record in supplier_totals:
- if (record['model_year_id'] == model_year and record['credit_class_id'] == credit_type
- and record['weight_class_id'] == weight_type):
- print(record['total_value'])
- found = True
- record['total_value'] -= credit_value
- print(record)
- if record['total_value'] < 0:
- print('NOT ENOUGH CREDITS')
- has_enough = False
+ if (record['model_year_id'] == model_year and record[
+ 'credit_class_id'] == credit_type
+ and record['weight_class_id'] == weight_type):
+ found = True
+ record['total_value'] -= credit_value
+ if record['total_value'] < 0:
+ has_enough = False
if not found:
has_enough = False
if not has_enough:
- raise Exception('not enough credits')
+ raise ValidationError('not enough credits')
else:
# add to each dictionary (one broken down by years and the other not)
if credit_type not in credit_total_no_years:
@@ -223,7 +223,8 @@ def validate_transfer(transfer):
),
total_value=1 * credit_value,
update_user=transfer.update_user,
- weight_class_id=1
+ weight_class=WeightClass.objects.get(
+ weight_class_code='LDV')
)
for each_supplier in [initiating_supplier, recieving_supplier]:
reduce_total = each_supplier == transfer.debit_from
@@ -259,6 +260,3 @@ def validate_transfer(transfer):
credit_transaction=added_transaction,
organization_id=each_supplier.id
)
-
-
-
diff --git a/backend/api/services/sales_spreadsheet.py b/backend/api/services/sales_spreadsheet.py
index 98323cf48..9c96ebcd1 100644
--- a/backend/api/services/sales_spreadsheet.py
+++ b/backend/api/services/sales_spreadsheet.py
@@ -8,6 +8,7 @@
from dateutil.parser import parse
from django.core.exceptions import ValidationError
+from django.db.models import Subquery
import xlrd
import xlwt
@@ -430,12 +431,13 @@ def create_errors_spreadsheet(submission_id, organization_id, stream):
worksheet.write(row, 4, 'Sales Date', style=BOLD)
worksheet.write(row, 5, 'Error', style=BOLD)
- record_of_sales_vin = RecordOfSale.objects.filter(
+ record_of_sales_vin = Subquery(RecordOfSale.objects.filter(
submission_id=submission_id
- ).values('vin')
+ ).values_list('vin', flat=True))
submission_content = SalesSubmissionContent.objects.filter(
- submission_id=submission_id
+ submission_id=submission_id,
+ submission__organization_id=organization.id
).exclude(
xls_vin__in=record_of_sales_vin
)
diff --git a/backend/api/tests/test_credit_transfers.py b/backend/api/tests/test_credit_transfers.py
index 16dd1cd35..e33583c41 100644
--- a/backend/api/tests/test_credit_transfers.py
+++ b/backend/api/tests/test_credit_transfers.py
@@ -1,8 +1,17 @@
from django.utils.datetime_safe import datetime
-from rest_framework.serializers import ValidationError
+from django.core.exceptions import ValidationError
from .base_test_case import BaseTestCase
from ..models.credit_transfer import CreditTransfer
+from ..models.credit_transaction import CreditTransaction
+from ..models.credit_transfer_content import CreditTransferContent
+from ..models.credit_class import CreditClass
+from ..models.model_year import ModelYear
+from ..models.weight_class import WeightClass
+from ..models.credit_transaction_type import CreditTransactionType
+from ..services.credit_transaction import validate_transfer
+from ..models.account_balance import AccountBalance
+
class TestTransfers(BaseTestCase):
def setUp(self):
@@ -31,6 +40,8 @@ def setUp(self):
debit_from=org2,
)
+
+
def test_list_transfer(self):
response = self.clients['RTAN_BCEID'].get("/api/credit-transfers")
self.assertEqual(response.status_code, 200)
@@ -48,3 +59,88 @@ def test_list_transfer_gov(self):
self.assertEqual(response.status_code, 200)
result = response.data
self.assertEqual(len(result), 1)
+
+ # test that if the supplier does not have enough credits of specific
+ # type for specified year the transfer will fail and the database
+ # won't change
+ def test_transfer_fail(self):
+ transfer_not_enough = CreditTransfer.objects.create(
+ debit_from=self.users['RTAN_BCEID'].organization,
+ status='RECOMMEND_APPROVAL',
+ credit_to=self.users['EMHILLIE_BCEID'].organization,
+ )
+ transfer_content = CreditTransferContent.objects.create(
+ model_year=ModelYear.objects.get(name='2020'),
+ credit_class=CreditClass.objects.get(credit_class="A"),
+ weight_class=WeightClass.objects.get(weight_class_code='LDV'),
+ credit_value=10,
+ dollar_value=10,
+ credit_transfer=transfer_not_enough,
+ )
+
+ # try changing from status RECOMMENDED to ISSUED, this should fail
+ # ie it should throw a Validation Error
+ self.assertRaises(
+ ValidationError, validate_transfer, transfer_not_enough)
+
+
+ # test that if the supplier does have enough of credit type/year the
+ # transfer will work, a record will be added for each row in transfer
+ # content (or unique credit type/year/weight) in the transaction table,
+ # and a new row for each credit type for each organization in the
+ # account balance table.
+
+ def test_transfer_pass(self):
+ transaction = CreditTransaction.objects.create(
+ credit_to=self.users['EMHILLIE_BCEID'].organization,
+ model_year=ModelYear.objects.get(name='2020'),
+ credit_class=CreditClass.objects.get(credit_class="A"),
+ weight_class=WeightClass.objects.get(weight_class_code='LDV'),
+ credit_value=4,
+ number_of_credits=100,
+ total_value=400,
+ transaction_type=CreditTransactionType.objects.get(
+ transaction_type="Validation")
+ )
+ transfer_enough = CreditTransfer.objects.create(
+ credit_to=self.users['RTAN_BCEID'].organization,
+ status='RECOMMEND_APPROVAL',
+ debit_from=self.users['EMHILLIE_BCEID'].organization,
+ update_user=self.users['EMHILLIE_BCEID'],
+ )
+
+ transfer_content = CreditTransferContent.objects.create(
+ model_year=ModelYear.objects.get(name='2020'),
+ credit_class=CreditClass.objects.get(credit_class="A"),
+ weight_class=WeightClass.objects.get(weight_class_code='LDV'),
+ credit_value=10,
+ dollar_value=10,
+ credit_transfer=transfer_enough,
+ )
+
+ validate_transfer(transfer_enough)
+
+ seller_balance = AccountBalance.objects.filter(
+ organization_id=self.users['EMHILLIE_BCEID'].organization.id,
+ expiration_date=None,
+ credit_class=CreditClass.objects.get(credit_class="A")
+ ).first()
+
+ buyer_balance = AccountBalance.objects.filter(
+ organization_id=self.users['RTAN_BCEID'].organization.id,
+ expiration_date=None,
+ credit_class=CreditClass.objects.get(credit_class="A")
+ ).first()
+
+ self.assertEqual(seller_balance.balance, -10)
+ self.assertEqual(buyer_balance.balance, 10)
+
+ credit_transaction_record = CreditTransaction.objects.filter(
+ credit_to=self.users['RTAN_BCEID'].organization,
+ debit_from=self.users['EMHILLIE_BCEID'].organization,
+ credit_class=CreditClass.objects.get(credit_class="A"),
+ ).order_by('-id').first()
+ self.assertEqual(credit_transaction_record.id, seller_balance.credit_transaction_id)
+
+
+
diff --git a/backend/api/tests/test_vehicles.py b/backend/api/tests/test_vehicles.py
index 701cdb561..dcb9b8459 100644
--- a/backend/api/tests/test_vehicles.py
+++ b/backend/api/tests/test_vehicles.py
@@ -2,14 +2,35 @@
from .base_test_case import BaseTestCase
from ..models.vehicle import Vehicle
+from ..models.user_role import UserRole
+from ..models.role import Role
class TestVehicles(BaseTestCase):
def test_get_vehicles(self):
+ UserRole.objects.create(
+ user_profile_id=self.users['RTAN_BCEID'].id,
+ role=Role.objects.get(
+ role_code='ZEVA User',
+ )
+ )
+
response = self.clients['RTAN_BCEID'].get("/api/vehicles")
self.assertEqual(response.status_code, 200)
def test_update_vehicle_state(self):
+ UserRole.objects.create(
+ user_profile_id=self.users['RTAN_BCEID'].id,
+ role=Role.objects.get(
+ role_code='Signing Authority',
+ )
+ )
+ UserRole.objects.create(
+ user_profile_id=self.users['RTAN_BCEID'].id,
+ role=Role.objects.get(
+ role_code='ZEVA User',
+ )
+ )
org1 = self.users['RTAN_BCEID'].organization
vehicle = Vehicle.objects.filter(organization=org1).first()
diff --git a/backend/api/viewsets/credit_request.py b/backend/api/viewsets/credit_request.py
index 078b55344..5c918e655 100644
--- a/backend/api/viewsets/credit_request.py
+++ b/backend/api/viewsets/credit_request.py
@@ -1,17 +1,27 @@
import json
from datetime import datetime
+from django.core.paginator import Paginator
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
-from django.http import HttpResponse
+from django.db.models import Subquery, Count
+from django.db.models import Q
+from django.http import HttpResponse, HttpResponseForbidden
+
from rest_framework import mixins, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
+from rest_framework.response import Response
+from api.models.icbc_registration_data import IcbcRegistrationData
+from api.models.record_of_sale import RecordOfSale
from api.models.sales_submission import SalesSubmission
+from api.models.sales_submission_content import SalesSubmissionContent
from api.models.sales_submission_statuses import SalesSubmissionStatuses
from api.serializers.sales_submission import SalesSubmissionSerializer, \
SalesSubmissionListSerializer, SalesSubmissionSaveSerializer
+from api.serializers.sales_submission_content import \
+ SalesSubmissionContentSerializer
from api.services.credit_transaction import award_credits
from api.services.sales_spreadsheet import create_sales_spreadsheet, \
ingest_sales_spreadsheet, validate_spreadsheet, \
@@ -50,6 +60,7 @@ def get_queryset(self):
'retrieve': SalesSubmissionSerializer,
'partial_update': SalesSubmissionSaveSerializer,
'update': SalesSubmissionSaveSerializer,
+ 'content': SalesSubmissionContentSerializer,
}
def get_serializer_class(self):
@@ -149,3 +160,113 @@ def download_errors(self, request, pk):
)
)
return response
+
+ @action(detail=True)
+ def content(self, request, pk):
+ filters = request.GET.get('filters')
+ page_size = request.GET.get('page_size', 20)
+ page = request.GET.get('page', 1)
+ sort_by = request.GET.get('sorted')
+
+ try:
+ page = int(page)
+ except:
+ page = 1
+
+ if page < 1:
+ page = 1
+
+ # only government should be able to view the contents for icbc
+ # verification
+ if not request.user.is_government:
+ return HttpResponseForbidden()
+
+ submission_content = SalesSubmissionContent.objects.filter(
+ submission_id=pk
+ )
+
+ if filters:
+ submission_filters = json.loads(filters)
+
+ if 'xls_make' in submission_filters:
+ submission_content = submission_content.filter(
+ xls_make__icontains=submission_filters['xls_make']
+ )
+
+ if 'xls_model' in submission_filters:
+ submission_content = submission_content.filter(
+ xls_model__icontains=submission_filters['xls_model']
+ )
+
+ if 'xls_model_year' in submission_filters:
+ submission_content = submission_content.filter(
+ xls_model_year__icontains=submission_filters['xls_model_year']
+ )
+
+ if 'xls_vin' in submission_filters:
+ submission_content = submission_content.filter(
+ xls_vin__icontains=submission_filters['xls_vin']
+ )
+
+ if 'warning' in submission_filters:
+ duplicate_vins = Subquery(submission_content.annotate(
+ vin_count=Count('xls_vin')
+ ).filter(vin_count__gt=1).values_list('xls_vin', flat=True))
+
+ awarded_vins = Subquery(RecordOfSale.objects.exclude(
+ submission_id=pk
+ ).values_list('vin', flat=True))
+
+ submission_content = submission_content.filter(
+ Q(xls_vin__in=duplicate_vins) |
+ Q(xls_vin__in=awarded_vins) |
+ ~Q(xls_vin__in=Subquery(
+ IcbcRegistrationData.objects.values('vin')
+ )) |
+ Q(xls_sale_date__lte="43102.0")
+ )
+
+ if sort_by:
+ order_by = []
+ sort_by_list = sort_by.split(',')
+ for sort in sort_by_list:
+ if sort in [
+ 'xls_make', 'xls_model', 'xls_model_year',
+ 'xls_sale_date', 'xls_vin',
+ '-xls_make', '-xls_model', '-xls_model_year',
+ '-xls_sale_date', '-xls_vin',
+ ]:
+ order_by.append(sort)
+
+ if order_by:
+ submission_content = submission_content.order_by(*order_by)
+
+ submission_content_paginator = Paginator(submission_content, page_size)
+
+ paginated = submission_content_paginator.page(page)
+
+ serializer = SalesSubmissionContentSerializer(
+ paginated, many=True, read_only=True, context={'request': request}
+ )
+
+ return Response({
+ 'content': serializer.data,
+ 'pages': submission_content_paginator.num_pages
+ })
+
+ @action(detail=True)
+ def unselected(self, request, pk):
+ if not request.user.is_government:
+ return HttpResponseForbidden()
+
+ selected_vins = Subquery(RecordOfSale.objects.filter(
+ submission_id=pk
+ ).values_list('vin', flat=True))
+
+ unselected_vins = SalesSubmissionContent.objects.filter(
+ submission_id=pk
+ ).exclude(
+ xls_vin__in=selected_vins
+ ).values_list('id', flat=True)
+
+ return Response(list(unselected_vins))
diff --git a/frontend/package.json b/frontend/package.json
index cd23f8b1c..7a226ab4d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "zeva-frontend",
- "version": "1.15.0",
+ "version": "1.15.0-1",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
diff --git a/frontend/src/app/routes/CreditRequests.js b/frontend/src/app/routes/CreditRequests.js
index 4a75501e8..df1deb5bc 100644
--- a/frontend/src/app/routes/CreditRequests.js
+++ b/frontend/src/app/routes/CreditRequests.js
@@ -2,12 +2,14 @@ const API_BASE_PATH = '/credit-requests';
const CREDIT_REQUESTS = {
CONFIRM: `${API_BASE_PATH}/:id/confirm`,
+ CONTENT: `${API_BASE_PATH}/:id/content`,
DETAILS: `${API_BASE_PATH}/:id`,
DOWNLOAD_ERRORS: `${API_BASE_PATH}/:id/download_errors`,
EDIT: `${API_BASE_PATH}/:id/edit`,
LIST: API_BASE_PATH,
NEW: `${API_BASE_PATH}/new`,
TEMPLATE: `${API_BASE_PATH}/template`,
+ UNSELECTED: `${API_BASE_PATH}/:id/unselected`,
UPLOAD: `${API_BASE_PATH}/upload`,
VALIDATE: `${API_BASE_PATH}/:id/validate`,
VALIDATED: `${API_BASE_PATH}/:id/validated`,
diff --git a/frontend/src/credits/CreditRequestVINListContainer.js b/frontend/src/credits/CreditRequestVINListContainer.js
index f85b86be3..98ddf81ea 100644
--- a/frontend/src/credits/CreditRequestVINListContainer.js
+++ b/frontend/src/credits/CreditRequestVINListContainer.js
@@ -16,34 +16,28 @@ const CreditRequestVINListContainer = (props) => {
const { match, user } = props;
const { id } = match.params;
+ const [content, setContent] = useState([]);
const [submission, setSubmission] = useState([]);
const [loading, setLoading] = useState(true);
- const [validatedList, setValidatedList] = useState([]);
+ const [invalidatedList, setInvalidatedList] = useState([]);
const refreshDetails = () => {
- axios.get(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id)).then((response) => {
- const { data } = response;
- setSubmission(data);
-
- const validatedRecords = data.content.filter(
- (record) => {
- if (record.warnings && record.warnings.some((warning) => [
- 'DUPLICATE_VIN', 'INVALID_MODEL', 'VIN_ALREADY_AWARDED',
- ].indexOf(warning) >= 0)) {
- return false;
- }
-
- if (data.validationStatus === 'CHECKED') {
- return record.recordOfSale;
- }
-
- return record.icbcVerification;
- },
- ).map((record) => record.id);
-
- setValidatedList(validatedRecords);
+ axios.all([
+ axios.get(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id)),
+ axios.get(ROUTES_CREDIT_REQUESTS.CONTENT.replace(':id', id)),
+ axios.get(ROUTES_CREDIT_REQUESTS.UNSELECTED.replace(':id', id)),
+ ]).then(axios.spread((submissionResponse, contentResponse, unselectedResponse) => {
+ const { data: submissionData } = submissionResponse;
+ setSubmission(submissionData);
+
+ const { data } = contentResponse;
+ setContent(data.content);
+
+ const { data: unselected } = unselectedResponse;
+ setInvalidatedList(unselected);
+
setLoading(false);
- });
+ }));
};
useEffect(() => {
@@ -54,15 +48,17 @@ const CreditRequestVINListContainer = (props) => {
const { value: submissionId, checked } = event.target;
const newId = Number(submissionId);
if (!checked) {
- setValidatedList(validatedList.filter((item) => Number(item) !== Number(submissionId)));
+ setInvalidatedList(() => [...invalidatedList, newId]);
} else {
- setValidatedList(() => [...validatedList, newId]);
+ setInvalidatedList(invalidatedList.filter((item) => Number(item) !== Number(submissionId)));
}
};
const handleSubmit = () => {
+ setLoading(true);
+
axios.patch(ROUTES_CREDIT_REQUESTS.DETAILS.replace(':id', id), {
- records: validatedList,
+ invalidated: invalidatedList,
validationStatus: 'CHECKED',
}).then(() => {
const url = ROUTES_CREDIT_REQUESTS.VALIDATED.replace(/:id/g, submission.id);
@@ -77,12 +73,14 @@ const CreditRequestVINListContainer = (props) => {
return (
);
};
diff --git a/frontend/src/credits/UploadCreditRequestContainer.js b/frontend/src/credits/UploadCreditRequestContainer.js
index 2226bfa67..227e16098 100644
--- a/frontend/src/credits/UploadCreditRequestContainer.js
+++ b/frontend/src/credits/UploadCreditRequestContainer.js
@@ -8,6 +8,7 @@ import { useParams } from 'react-router-dom';
import CreditTransactionTabs from '../app/components/CreditTransactionTabs';
import history from '../app/History';
+import Loading from '../app/components/Loading';
import ROUTES_CREDIT_REQUESTS from '../app/routes/CreditRequests';
import CustomPropTypes from '../app/utilities/props';
import { upload } from '../app/utilities/upload';
@@ -17,10 +18,12 @@ const UploadCreditRequestsContainer = (props) => {
const { user } = props;
const [errorMessage, setErrorMessage] = useState(null);
const [files, setFiles] = useState([]);
+ const [loading, setLoading] = useState(true);
const { id } = useParams();
const doUpload = () => {
+ setLoading(true);
let data = {};
if (id) {
@@ -40,11 +43,17 @@ const UploadCreditRequestsContainer = (props) => {
} else {
setErrorMessage('An error has occurred while uploading. Please try again later.');
}
+
+ setLoading(false);
});
};
useEffect(() => {}, []);
+ if (loading) {
+ return ();
+ }
+
return ([
,
{
const recordsAddress = submission.organization.organizationAddress.find((address) => address.addressType.addressType === 'Records');
const downloadErrors = (e) => {
- const element = e.target;
+ const element = e.currentTarget;
const original = element.innerHTML;
- element.firstChild.textContent = ' Downloading...';
+
+ element.innerText = 'Downloading...';
+ element.disabled = true;
+
return download(ROUTES_CREDIT_REQUESTS.DOWNLOAD_ERRORS.replace(':id', submission.id), {}).then(() => {
element.innerHTML = original;
+ element.disabled = false;
});
};
diff --git a/frontend/src/credits/components/CreditRequestListTable.js b/frontend/src/credits/components/CreditRequestListTable.js
index 4329052a8..72c757edf 100644
--- a/frontend/src/credits/components/CreditRequestListTable.js
+++ b/frontend/src/credits/components/CreditRequestListTable.js
@@ -47,9 +47,9 @@ const CreditRequestListTable = (props) => {
}, {
accessor: (item) => (item.totalWarnings > 0 ? item.totalWarnings : '-'),
className: 'text-right',
- Header: 'Warnings',
+ Header: 'Not Eligible for Credits',
id: 'warnings',
- maxWidth: 150,
+ maxWidth: 250,
}, {
accessor: (item) => (item.totalCredits && item.totalCredits.a > 0 ? formatNumeric(item.totalCredits.a) : '-'),
className: 'text-right',
diff --git a/frontend/src/credits/components/CreditRequestVINListPage.js b/frontend/src/credits/components/CreditRequestVINListPage.js
index ef44c590d..11220f3da 100644
--- a/frontend/src/credits/components/CreditRequestVINListPage.js
+++ b/frontend/src/credits/components/CreditRequestVINListPage.js
@@ -7,11 +7,13 @@ import VINListTable from './VINListTable';
const CreditRequestVINListPage = (props) => {
const {
+ content,
handleCheckboxClick,
handleSubmit,
+ setContent,
submission,
user,
- validatedList,
+ invalidatedList,
} = props;
const [filtered, setFiltered] = useState([]);
@@ -90,13 +92,15 @@ const CreditRequestVINListPage = (props) => {
@@ -113,14 +117,15 @@ const CreditRequestVINListPage = (props) => {
CreditRequestVINListPage.defaultProps = {};
CreditRequestVINListPage.propTypes = {
+ content: PropTypes.arrayOf(PropTypes.shape()).isRequired,
handleCheckboxClick: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
- submission: PropTypes.shape().isRequired,
- user: CustomPropTypes.user.isRequired,
- validatedList: PropTypes.arrayOf(PropTypes.oneOfType([
+ invalidatedList: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
])).isRequired,
+ submission: PropTypes.shape().isRequired,
+ user: CustomPropTypes.user.isRequired,
};
export default CreditRequestVINListPage;
diff --git a/frontend/src/credits/components/CreditTransfersDetailsPage.js b/frontend/src/credits/components/CreditTransfersDetailsPage.js
index 2959d0f70..43b6b4ed3 100644
--- a/frontend/src/credits/components/CreditTransfersDetailsPage.js
+++ b/frontend/src/credits/components/CreditTransfersDetailsPage.js
@@ -196,11 +196,11 @@ const CreditTransfersDetailsPage = (props) => {
Light Duty Vehicle Credit Transfer
{permissions.governmentDirector && submission.creditTransferComment
- && (
-
-
-
- )}
+ && (
+
+
+
+ )}
{permissions.governmentAnalyst && negativeCredit
&& (
diff --git a/frontend/src/credits/components/SubmissionListTable.js b/frontend/src/credits/components/SubmissionListTable.js
deleted file mode 100644
index 6c4fefe31..000000000
--- a/frontend/src/credits/components/SubmissionListTable.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Presentational component
- */
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import CustomPropTypes from '../../app/utilities/props';
-import ReactTable from '../../app/components/ReactTable';
-import formatNumeric from '../../app/utilities/formatNumeric';
-import formatStatus from '../../app/utilities/formatStatus';
-import history from '../../app/History';
-import ROUTES_CREDITS from '../../app/routes/Credits';
-
-const SubmissionListTable = (props) => {
- const {
- items, filtered, setFiltered, user,
- } = props;
-
- const columns = [{
- accessor: 'id',
- className: 'text-right',
- Header: 'ID',
- maxWidth: 75,
- }, {
- accessor: 'submissionDate',
- className: 'text-center',
- Header: 'Date',
- }, {
- accessor: 'updateUser.displayName',
- className: 'text-left',
- Header: 'Last User',
- id: 'updateUser',
- }, {
- accessor: (item) => (item.organization ? item.organization.name : ''),
- className: 'text-left',
- Header: 'Supplier',
- id: 'supplier',
- show: user.isGovernment,
- }, {
- accessor: (item) => {
- if (['DRAFT', 'SUBMITTED'].indexOf(item.validationStatus) >= 0) {
- const totals = item.totals.vins + item.unselected;
- return (totals > 0 ? totals : '-');
- }
-
- return (item.totals.vins > 0 ? item.totals.vins : '-');
- },
- className: 'text-right',
- Header: 'Total Sales',
- maxWidth: 150,
- id: 'total-sales',
- }, {
- accessor: (item) => (item.totalWarnings > 0 ? item.totalWarnings : '-'),
- className: 'text-right',
- Header: 'Warnings',
- id: 'warnings',
- maxWidth: 150,
- }, {
- accessor: (item) => (item.totalACredits > 0 ? formatNumeric(item.totalACredits) : '-'),
- className: 'text-right',
- Header: 'A-Credits',
- id: 'credits-a',
- maxWidth: 150,
- }, {
- accessor: (item) => (item.totalBCredits > 0 ? formatNumeric(item.totalBCredits) : '-'),
- className: 'text-right',
- Header: 'B-Credits',
- id: 'credits-b',
- maxWidth: 150,
- }, {
- accessor: (item) => {
- const { validationStatus } = item;
- const status = formatStatus(validationStatus);
-
- if (status === 'checked') {
- return 'validated';
- }
-
- if (status === 'validated') {
- return 'issued';
- }
-
- if (status === 'recommend approval') {
- return 'recommend issuance';
- }
-
- return status;
- },
- className: 'text-center text-capitalize',
- filterMethod: (filter, row) => {
- const filterValues = filter.value.split(',');
-
- let returnValue = false;
-
- filterValues.forEach((filterValue) => {
- const value = filterValue.toLowerCase().trim();
-
- if (value !== '' && !returnValue) {
- returnValue = row[filter.id].toLowerCase().includes(value);
- }
- });
-
- return returnValue;
- },
- Header: 'Status',
- id: 'status',
- maxWidth: 250,
- }];
-
- return (
- {
- if (row && row.original) {
- return {
- onClick: () => {
- const { id } = row.original;
-
- history.push(ROUTES_CREDITS.CREDIT_REQUEST_DETAILS.replace(/:id/g, id), filtered);
- },
- className: 'clickable',
- };
- }
-
- return {};
- }}
- setFiltered={setFiltered}
- />
- );
-};
-
-SubmissionListTable.defaultProps = {
- filtered: undefined,
- setFiltered: undefined,
-};
-
-SubmissionListTable.propTypes = {
- filtered: PropTypes.arrayOf(PropTypes.shape()),
- items: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
- setFiltered: PropTypes.func,
- user: CustomPropTypes.user.isRequired,
-};
-
-export default SubmissionListTable;
diff --git a/frontend/src/credits/components/VINListTable.js b/frontend/src/credits/components/VINListTable.js
index bc8160ac4..b0c0b77fe 100644
--- a/frontend/src/credits/components/VINListTable.js
+++ b/frontend/src/credits/components/VINListTable.js
@@ -1,24 +1,31 @@
/*
* Presentational component
*/
+import axios from 'axios';
import moment from 'moment-timezone';
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState } from 'react';
+import ReactTable from 'react-table';
-import ReactTable from '../../app/components/ReactTable';
import CREDIT_ERROR_CODES from '../../app/constants/errorCodes';
+import ROUTES_CREDIT_REQUESTS from '../../app/routes/CreditRequests';
import CustomPropTypes from '../../app/utilities/props';
const VINListTable = (props) => {
const {
handleCheckboxClick,
+ id,
items,
user,
- validatedList,
+ invalidatedList,
filtered,
+ setContent,
setFiltered,
} = props;
+ const [loading, setLoading] = useState(false);
+ const [pages, setPages] = useState(-1);
+
const getErrorCodes = (item, fields = false) => {
let errorCodes = '';
@@ -57,20 +64,21 @@ const VINListTable = (props) => {
},
className: 'text-center',
Header: 'MY',
- id: 'model-year',
+ id: 'xls_model_year',
}, {
accessor: 'xlsMake',
Header: 'Make',
- id: 'make',
+ id: 'xls_make',
}, {
accessor: 'xlsModel',
Header: 'Model',
- id: 'model',
+ id: 'xls_model',
}, {
accessor: (row) => (moment(row.salesDate).format('YYYY-MM-DD') !== 'Invalid date' ? moment(row.salesDate).format('YYYY-MM-DD') : row.salesDate),
className: 'text-center sales-date',
+ filterable: false,
Header: 'Retail Sale',
- id: 'sales-date',
+ id: 'xls_sale_date',
}],
}, {
Header: '',
@@ -80,7 +88,7 @@ const VINListTable = (props) => {
className: 'vin',
Header: 'VIN',
headerClassName: 'vin',
- id: 'vin',
+ id: 'xls_vin',
minWidth: 150,
}],
}, {
@@ -89,17 +97,23 @@ const VINListTable = (props) => {
columns: [{
accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.modelYear.name : '-'),
className: 'icbc-model-year text-center',
+ filterable: false,
+ sortable: false,
Header: 'MY',
headerClassName: 'icbc-model-year',
id: 'icbc-model-year',
}, {
accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.make : '-'),
className: 'icbc-make',
+ filterable: false,
+ sortable: false,
Header: 'Make',
id: 'icbc-make',
}, {
accessor: (item) => (item.icbcVerification ? item.icbcVerification.icbcVehicle.modelName : '-'),
className: 'icbc-model',
+ filterable: false,
+ sortable: false,
Header: 'Model',
id: 'icbc-model',
}],
@@ -123,7 +137,7 @@ const VINListTable = (props) => {
return (
Number(item) === Number(row.id)) >= 0
+ invalidatedList.findIndex((item) => Number(item) === Number(row.id)) < 0
}
onChange={(event) => { handleCheckboxClick(event); }}
type="checkbox"
@@ -132,6 +146,8 @@ const VINListTable = (props) => {
);
},
className: 'text-center validated',
+ filterable: false,
+ sortable: false,
Header: 'Validated',
id: 'validated',
show: user.isGovernment,
@@ -143,11 +159,12 @@ const VINListTable = (props) => {
columns={columns}
data={items}
filtered={filtered}
+ filterable
onFilteredChange={(input) => {
setFiltered(input);
}}
defaultSorted={[{
- id: 'warning',
+ id: 'xls_vin',
desc: true,
}]}
getTrProps={(state, rowInfo) => {
@@ -186,6 +203,45 @@ const VINListTable = (props) => {
}
return {};
}}
+ loading={loading}
+ manual
+ onFetchData={(state) => {
+ setLoading(true);
+
+ const filters = {};
+
+ state.filtered.forEach((each) => {
+ filters[each.id] = each.value;
+ });
+
+ const sorted = [];
+
+ state.sorted.forEach((each) => {
+ let value = each.id;
+
+ if (each.desc) {
+ value = `-${value}`;
+ }
+
+ sorted.push(value);
+ });
+
+ axios.get(ROUTES_CREDIT_REQUESTS.CONTENT.replace(':id', id), {
+ params: {
+ filters,
+ page: state.page + 1, // page from front-end is zero index, but in the back-end we need the actual page number
+ page_size: state.pageSize,
+ sorted: sorted.join(','),
+ },
+ }).then((response) => {
+ const { content, pages: numPages } = response.data;
+
+ setContent(content);
+ setPages(numPages);
+ setLoading(false);
+ });
+ }}
+ pages={pages}
/>
);
};
@@ -199,7 +255,7 @@ VINListTable.propTypes = {
handleCheckboxClick: PropTypes.func.isRequired,
items: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
user: CustomPropTypes.user.isRequired,
- validatedList: PropTypes.arrayOf(PropTypes.oneOfType([
+ invalidatedList: PropTypes.arrayOf(PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
])).isRequired,
diff --git a/openshift-v3/templates/backend/README.md b/openshift-v3/templates/backend/README.md
index e7e441915..303b1798d 100644
--- a/openshift-v3/templates/backend/README.md
+++ b/openshift-v3/templates/backend/README.md
@@ -15,6 +15,9 @@
2. Create template secret template.django-secret
* oc process -f django-secret-template.yaml | oc create -f - -n [project namespace]
+3. Create email service secret for each environment
+ * oc process -f email-service-secret.yaml EMAIL_SERVICE_CLIENT_ID= EMAIL_SERVICE_CLIENT_SECRET= CHES_AUTH_URL= CHES_EMAIL_URL= | oc create -f - -n [env namespace]
+
#### After pipeline completes
1. After pipeline completes, create autoscaler for backend
diff --git a/openshift-v3/templates/backend/backend-dc.yaml b/openshift-v3/templates/backend/backend-dc.yaml
index 2f8909ef6..b6c6b88fe 100644
--- a/openshift-v3/templates/backend/backend-dc.yaml
+++ b/openshift-v3/templates/backend/backend-dc.yaml
@@ -318,16 +318,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: rabbitmq_port
- - name: SMTP_SERVER_HOST
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_host
- - name: SMTP_SERVER_PORT
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_port
- name: EMAIL_SENDING_ENABLED
valueFrom:
configMapKeyRef:
@@ -362,6 +352,26 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: minio_endpoint
+ - name: EMAIL_SERVICE_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: EMAIL_SERVICE_CLIENT_ID
+ - name: EMAIL_SERVICE_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: EMAIL_SERVICE_CLIENT_SECRET
+ - name: CHES_AUTH_URL
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: CHES_AUTH_URL
+ - name: CHES_EMAIL_URL
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: CHES_EMAIL_URL
livenessProbe:
failureThreshold: 30
tcpSocket:
diff --git a/openshift-v3/templates/backend/email-service-secret.yaml b/openshift-v3/templates/backend/email-service-secret.yaml
new file mode 100644
index 000000000..2de0bf6c9
--- /dev/null
+++ b/openshift-v3/templates/backend/email-service-secret.yaml
@@ -0,0 +1,26 @@
+apiVersion: template.openshift.io/v1
+kind: Template
+parameters:
+- name: EMAIL_SERVICE_CLIENT_ID
+ description: the client id for Zeva project
+ required: true
+- name: EMAIL_SERVICE_CLIENT_SECRET
+ description: the secrete for Zeva project
+ required: true
+- name: CHES_AUTH_URL
+ description: the authentication url to retrieve token
+ required: true
+- name: CHES_EMAIL_URL
+ description: the email service url
+ required: true
+objects:
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ annotations: null
+ name: email-service
+ stringData:
+ EMAIL_SERVICE_CLIENT_ID: ${EMAIL_SERVICE_CLIENT_ID}
+ EMAIL_SERVICE_CLIENT_SECRET: ${EMAIL_SERVICE_CLIENT_SECRET}
+ CHES_AUTH_URL: ${CHES_AUTH_URL}
+ CHES_EMAIL_URL: ${CHES_EMAIL_URL}
\ No newline at end of file
diff --git a/openshift-v3/templates/config/configmap.yaml b/openshift-v3/templates/config/configmap.yaml
index 8f1a63308..28d778349 100644
--- a/openshift-v3/templates/config/configmap.yaml
+++ b/openshift-v3/templates/config/configmap.yaml
@@ -44,7 +44,11 @@ parameters:
- name: OCP_NAME
displayName: Openshift Cluster Name
description: Openshift Cluster Name
- required: true
+ required: true
+ - name: LOGOUT_HOST
+ displayName: the logout host
+ description: the logout host
+ required: true
objects:
- apiVersion: v1
kind: ConfigMap
@@ -65,7 +69,7 @@ objects:
keycloak_certs_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs
keycloak_client_id: zeva
keycloak_issuer: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
- keycloak_logout_redirect_url: https://logontest.gov.bc.ca/clp-cgi/logoff.cgi?returl=https%3A%2F%2F${HOST_NAME}%2F
+ keycloak_logout_redirect_url: https://${LOGOUT_HOST}/clp-cgi/logoff.cgi?returl=https://${HOST_NAME}&retnow=1
keycloak_realm_name: ${KEYCLOAK_REALM}
keycloak_realm_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
keycloak_realm: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
@@ -80,5 +84,3 @@ objects:
rabbitmq_host: ${NAME}${SUFFIX}-rabbitmq-cluster.tbiwaq-${ENV_NAME}.svc.cluster.local
rabbitmq_port: '5672'
rabbitmq_vhost: /zeva
- smtp_server_host: apps.smtp.gov.bc.ca
- smtp_server_port: '25'
diff --git a/openshift-v3/templates/frontend/frontend-dc.yaml b/openshift-v3/templates/frontend/frontend-dc.yaml
index 45bb0c684..6afe51494 100644
--- a/openshift-v3/templates/frontend/frontend-dc.yaml
+++ b/openshift-v3/templates/frontend/frontend-dc.yaml
@@ -252,7 +252,7 @@ objects:
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- - mountPath: /app/src/app/config
+ - mountPath: /opt/app-root/src/src/app/config
name: ${NAME}-frontend-config${SUFFIX}
dnsPolicy: ClusterFirst
restartPolicy: Always
diff --git a/openshift-v3/templates/nagios/nagios-dc.yaml b/openshift-v3/templates/nagios/nagios-dc.yaml
index a80e561fb..327991f4b 100644
--- a/openshift-v3/templates/nagios/nagios-dc.yaml
+++ b/openshift-v3/templates/nagios/nagios-dc.yaml
@@ -203,8 +203,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config-${ENV_NAME}
key: keycloak_realm
- - name: SMTP_SERVER_HOST
- value: apps.smtp.gov.bc.ca
- name: DATABASE_SERVICE_NAME
valueFrom:
configMapKeyRef:
diff --git a/openshift-v3/templates/patroni/README.md b/openshift-v3/templates/patroni/README.md
index d7c3c72e3..28df32a91 100644
--- a/openshift-v3/templates/patroni/README.md
+++ b/openshift-v3/templates/patroni/README.md
@@ -34,7 +34,7 @@ login to patroni-backup pod and run backup.sh -1
5. Recover the backup to paroni database on Openshift v4
login patroini-backup pod on Openshift v4, run the following command
-./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3/postgresql-zeva_2020-08-28_19-06-28.sql.gz
+./backup.sh -r patroni-master-prod:5432/zeva -f /backups/fromv3 //yes, folder name only, it will pickup the file and ask confirmation
6. Verify the database on Openshift v3 and v4 to make sure they are same
diff --git a/openshift-v3/templates/unittest/backend-dc-unittest.yaml b/openshift-v3/templates/unittest/backend-dc-unittest.yaml
index 27f0b0e39..2bbeb15ce 100644
--- a/openshift-v3/templates/unittest/backend-dc-unittest.yaml
+++ b/openshift-v3/templates/unittest/backend-dc-unittest.yaml
@@ -230,16 +230,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: rabbitmq_port
- - name: SMTP_SERVER_HOST
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_host
- - name: SMTP_SERVER_PORT
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_port
- name: EMAIL_SENDING_ENABLED
valueFrom:
configMapKeyRef:
diff --git a/openshift/templates/backend/README.md b/openshift/templates/backend/README.md
index f6dc0d0a3..3ffc45c15 100644
--- a/openshift/templates/backend/README.md
+++ b/openshift/templates/backend/README.md
@@ -15,6 +15,9 @@
2. Create template secret template.django-secret
* oc process -f django-secret-template.yaml | oc create -f - -n [project namespace]
+3. Create email service secret for each environment
+ * oc process -f email-service-secret.yaml EMAIL_SERVICE_CLIENT_ID= EMAIL_SERVICE_CLIENT_SECRET= CHES_AUTH_URL= CHES_EMAIL_URL= | oc create -f - -n [env namespace]
+
#### After pipeline completes
1. After pipeline completes, create autoscaler for backend
diff --git a/openshift/templates/backend/backend-dc.yaml b/openshift/templates/backend/backend-dc.yaml
index 957167eb6..b32fed73c 100644
--- a/openshift/templates/backend/backend-dc.yaml
+++ b/openshift/templates/backend/backend-dc.yaml
@@ -314,16 +314,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: rabbitmq_port
- - name: SMTP_SERVER_HOST
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_host
- - name: SMTP_SERVER_PORT
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_port
- name: EMAIL_SENDING_ENABLED
valueFrom:
configMapKeyRef:
@@ -358,6 +348,26 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: minio_endpoint
+ - name: EMAIL_SERVICE_CLIENT_ID
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: EMAIL_SERVICE_CLIENT_ID
+ - name: EMAIL_SERVICE_CLIENT_SECRET
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: EMAIL_SERVICE_CLIENT_SECRET
+ - name: CHES_AUTH_URL
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: CHES_AUTH_URL
+ - name: CHES_EMAIL_URL
+ valueFrom:
+ secretKeyRef:
+ name: email-service
+ key: CHES_EMAIL_URL
livenessProbe:
failureThreshold: 30
tcpSocket:
diff --git a/openshift/templates/backend/email-service-secret.yaml b/openshift/templates/backend/email-service-secret.yaml
new file mode 100644
index 000000000..2de0bf6c9
--- /dev/null
+++ b/openshift/templates/backend/email-service-secret.yaml
@@ -0,0 +1,26 @@
+apiVersion: template.openshift.io/v1
+kind: Template
+parameters:
+- name: EMAIL_SERVICE_CLIENT_ID
+ description: the client id for Zeva project
+ required: true
+- name: EMAIL_SERVICE_CLIENT_SECRET
+ description: the secrete for Zeva project
+ required: true
+- name: CHES_AUTH_URL
+ description: the authentication url to retrieve token
+ required: true
+- name: CHES_EMAIL_URL
+ description: the email service url
+ required: true
+objects:
+- apiVersion: v1
+ kind: Secret
+ metadata:
+ annotations: null
+ name: email-service
+ stringData:
+ EMAIL_SERVICE_CLIENT_ID: ${EMAIL_SERVICE_CLIENT_ID}
+ EMAIL_SERVICE_CLIENT_SECRET: ${EMAIL_SERVICE_CLIENT_SECRET}
+ CHES_AUTH_URL: ${CHES_AUTH_URL}
+ CHES_EMAIL_URL: ${CHES_EMAIL_URL}
\ No newline at end of file
diff --git a/openshift/templates/config/configmap.yaml b/openshift/templates/config/configmap.yaml
index 748b8c7ba..09eacfa8b 100644
--- a/openshift/templates/config/configmap.yaml
+++ b/openshift/templates/config/configmap.yaml
@@ -69,7 +69,7 @@ objects:
keycloak_certs_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs
keycloak_client_id: zeva
keycloak_issuer: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
- keycloak_logout_redirect_url: https://${LOGOUT_HOST_NAME}/clp-cgi/logoff.cgi?returl=https%3A%2F%2F${HOST_NAME}%2F
+ keycloak_logout_redirect_url: https://${LOGOUT_HOST_NAME}/clp-cgi/logoff.cgi?returl=https://${HOST_NAME}&retnow=1
keycloak_realm_name: ${KEYCLOAK_REALM}
keycloak_realm_url: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
keycloak_realm: https://${SSO_NAME}/auth/realms/${KEYCLOAK_REALM}
@@ -84,5 +84,3 @@ objects:
rabbitmq_host: ${NAME}${SUFFIX}-rabbitmq-cluster.e52f12-${ENV_NAME}.svc.cluster.local
rabbitmq_port: '5672'
rabbitmq_vhost: /zeva
- smtp_server_host: apps.smtp.gov.bc.ca
- smtp_server_port: '25'
diff --git a/openshift/templates/frontend/frontend-dc.yaml b/openshift/templates/frontend/frontend-dc.yaml
index 02121ea11..6c13ccdd6 100644
--- a/openshift/templates/frontend/frontend-dc.yaml
+++ b/openshift/templates/frontend/frontend-dc.yaml
@@ -248,7 +248,7 @@ objects:
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- - mountPath: /app/src/app/config
+ - mountPath: /opt/app-root/src/src/app/config
name: ${NAME}-frontend-config${SUFFIX}
dnsPolicy: ClusterFirst
restartPolicy: Always
diff --git a/openshift/templates/nagios/nagios-dc.yaml b/openshift/templates/nagios/nagios-dc.yaml
index 49af25b8f..3906b432a 100644
--- a/openshift/templates/nagios/nagios-dc.yaml
+++ b/openshift/templates/nagios/nagios-dc.yaml
@@ -203,8 +203,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config-${ENV_NAME}
key: keycloak_realm
- - name: SMTP_SERVER_HOST
- value: apps.smtp.gov.bc.ca
- name: DATABASE_SERVICE_NAME
valueFrom:
configMapKeyRef:
diff --git a/openshift/templates/nagios/nagios3/commands/check_email_connection.sh b/openshift/templates/nagios/nagios3/commands/check_email_connection.sh
index e49e5ebd4..5a07ee1b9 100755
--- a/openshift/templates/nagios/nagios3/commands/check_email_connection.sh
+++ b/openshift/templates/nagios/nagios3/commands/check_email_connection.sh
@@ -1,10 +1,13 @@
#!/bin/bash
-emailConnectionTest=$(python3 /etc/nagios3/commands/check_email_connection.py)
-echo $emailConnectionTest
-if [[ $emailConnectionTest == OK* ]];
-then
- exit 0
-else
- exit 2
-fi
+exit 0
+
+# comment out email connection checking as zeva switches to CHES
+#emailConnectionTest=$(python3 /etc/nagios3/commands/check_email_connection.py)
+#echo $emailConnectionTest
+#if [[ $emailConnectionTest == OK* ]];
+#then
+# exit 0
+#else
+# exit 2
+#fi
diff --git a/openshift/templates/patroni/README.md b/openshift/templates/patroni/README.md
index aca0503eb..a30702849 100644
--- a/openshift/templates/patroni/README.md
+++ b/openshift/templates/patroni/README.md
@@ -37,7 +37,7 @@ login to patroni-backup pod and run backup.sh -1
5. Recover the backup to paroni database on Openshift v4
login patroini-backup pod on Openshift v4, run the following command
-./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3/postgresql-zeva_2020-08-28_19-06-28.sql.gz
+./backup.sh -r patroni-master-prod/zeva -f /backups/fromv3 //yes, folder name only, it will pickup the file and ask confirmation
6. Verify the database on Openshift v3 and v4 to make sure they are same
diff --git a/openshift/templates/unittest/backend-dc-unittest.yaml b/openshift/templates/unittest/backend-dc-unittest.yaml
index a7fb74602..1bfaf7875 100644
--- a/openshift/templates/unittest/backend-dc-unittest.yaml
+++ b/openshift/templates/unittest/backend-dc-unittest.yaml
@@ -230,16 +230,6 @@ objects:
configMapKeyRef:
name: ${NAME}-config${SUFFIX}
key: rabbitmq_port
- - name: SMTP_SERVER_HOST
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_host
- - name: SMTP_SERVER_PORT
- valueFrom:
- configMapKeyRef:
- name: ${NAME}-config${SUFFIX}
- key: smtp_server_port
- name: EMAIL_SENDING_ENABLED
valueFrom:
configMapKeyRef: