diff --git a/migrations/sql/V2024.06.06.15.12__add_vc_connection_deleted_ind.sql b/migrations/sql/V2024.06.06.15.12__add_vc_connection_deleted_ind.sql new file mode 100644 index 0000000000..43a5fb7211 --- /dev/null +++ b/migrations/sql/V2024.06.06.15.12__add_vc_connection_deleted_ind.sql @@ -0,0 +1 @@ +ALTER TABLE party_verifiable_credential_connection ADD deleted_ind boolean default false; diff --git a/services/common/src/components/forms/FormWrapper.tsx b/services/common/src/components/forms/FormWrapper.tsx index 707f047d55..14eb1f03ba 100644 --- a/services/common/src/components/forms/FormWrapper.tsx +++ b/services/common/src/components/forms/FormWrapper.tsx @@ -56,7 +56,7 @@ NOTABLE OMISSIONS: SEE ALSO: - BaseInput.tsx */ -interface FormWrapperProps { +export interface FormWrapperProps { name: string; initialValues?: any; reduxFormConfig?: Partial; diff --git a/services/common/src/components/forms/RenderFileUpload.tsx b/services/common/src/components/forms/RenderFileUpload.tsx index 915f48a229..430dcf3eee 100644 --- a/services/common/src/components/forms/RenderFileUpload.tsx +++ b/services/common/src/components/forms/RenderFileUpload.tsx @@ -134,8 +134,11 @@ export const FileUpload = (props: FileUploadProps) => { const fileTypeList = listedFileTypes ?? Object.keys(acceptedFileTypesMap); const fileTypeDisplayString = fileTypeList.slice(0, -1).join(", ") + ", and " + fileTypeList.slice(-1); + const fileSize = props.maxFileSize + ? ` with max individual file size of ${props.maxFileSize}` + : ""; const secondLine = abbrevLabel - ? `
We accept most common ${fileTypeDisplayString} files
` + ? `
We accept most common ${fileTypeDisplayString} files${fileSize}.
` : `
Accepted filetypes: ${fileTypeDisplayString}
`; return `${labelInstruction}
${secondLine}`; }; diff --git a/services/common/src/components/forms/RenderSelect.tsx b/services/common/src/components/forms/RenderSelect.tsx index 3f503f8786..71b01a4c9c 100644 --- a/services/common/src/components/forms/RenderSelect.tsx +++ b/services/common/src/components/forms/RenderSelect.tsx @@ -61,6 +61,7 @@ export const RenderSelect: FC = ({ > + + + +
+ +
+ + + + +
+ Supporting Documents +
+
+ Upload any supporting document and draft of + + + Information Requirements Table (IRT) + + + following the official template here. It is required to upload your final IRT in the form provided to proceed to the final application. +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + File Name + + + + + + + + + + + +
+
+ Document Category + +
+ + Updated + + + + + + + + + + + +
+
+
+ + Updated By + + + + + + + + + + + +
+
+
+ No Data Yet +
+
+
+
+
+
+
+
+
+
+ + + + + +`; diff --git a/services/common/src/components/projectSummary/__snapshots__/ProjectDates.spec.tsx.snap b/services/common/src/components/projectSummary/__snapshots__/ProjectDates.spec.tsx.snap new file mode 100644 index 0000000000..f6e2e8ce34 --- /dev/null +++ b/services/common/src/components/projectSummary/__snapshots__/ProjectDates.spec.tsx.snap @@ -0,0 +1,404 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProjectDates renders properly 1`] = ` +
+
+

+ Project Dates +

+
+ These dates are for guidance and planning purposes only and do not reflect actual delivery dates. The + + + Major Mines Office + + + will work with you on a more definitive schedule. +
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
+
+
+
+
+ +
+
+
+ +
+`; diff --git a/services/common/src/constants/reducerTypes.ts b/services/common/src/constants/reducerTypes.ts index 8461448ee3..3338b2e453 100644 --- a/services/common/src/constants/reducerTypes.ts +++ b/services/common/src/constants/reducerTypes.ts @@ -322,3 +322,4 @@ export const VERIFIABLE_CREDENTIALS = "VERIFIABLE_CREDENTIALS"; export const CREATE_VC_WALLET_CONNECTION_INVITATION = "CREATE_VC_WALLET_CONNECTION_INVITATION"; export const FETCH_VC_WALLET_CONNECTION_INVITATIONS = "FETCH_VC_WALLET_CONNECTION_INVITATIONS"; export const ISSUE_VC = "ISSUE_VC"; +export const DELETE_VC_WALLET_CONNECTION = "DELETE_VC_WALLET_CONNECTION"; diff --git a/services/common/src/models/documents/document.ts b/services/common/src/models/documents/document.ts index 9b85b6c842..4eb28c2337 100644 --- a/services/common/src/models/documents/document.ts +++ b/services/common/src/models/documents/document.ts @@ -163,10 +163,10 @@ export class MineDocument implements IMineDocument { _userRoles: string[] = [], is_latest_version: boolean = this.is_latest_version ) { - const canModify = is_latest_version && !this.is_archived; - if (!this.mine_document_guid) return []; + const canModify = is_latest_version && !this.is_archived && this.mine_document_guid; + const canView = this.file_type === ".pdf" && this.document_manager_guid; return [ - this.file_type === ".pdf" && FileOperations.View, + canView && FileOperations.View, FileOperations.Download, isFeatureEnabled(Feature.DOCUMENTS_REPLACE_FILE) && canModify && FileOperations.Replace, canModify && FileOperations.Archive, diff --git a/services/common/src/redux/actionCreators/partiesActionCreator.ts b/services/common/src/redux/actionCreators/partiesActionCreator.ts index 9a94d31d4c..9e89e2100b 100644 --- a/services/common/src/redux/actionCreators/partiesActionCreator.ts +++ b/services/common/src/redux/actionCreators/partiesActionCreator.ts @@ -298,6 +298,28 @@ export const createPartyOrgBookEntity = ( .finally(() => dispatch(hideLoading("modal"))); }; +export const deletePartyOrgBookEntity = ( + partyGuid: string +): AppThunk>> => (dispatch) => { + dispatch(request(reducerTypes.PARTY_ORGBOOK_ENTITY)); + dispatch(showLoading("modal")); + return CustomAxios() + .delete(ENVIRONMENT.apiUrl + API.PARTY_ORGBOOK_ENTITY(partyGuid), createRequestHeader()) + .then((response) => { + dispatch(hideLoading("modal")); + notification.success({ + message: "Successfully disassociated party with OrgBook entity", + duration: 10, + }); + dispatch(success(reducerTypes.PARTY_ORGBOOK_ENTITY)); + return response; + }) + .catch(() => { + dispatch(error(reducerTypes.PARTY_ORGBOOK_ENTITY)); + }) + .finally(() => dispatch(hideLoading("modal"))); +}; + export const mergeParties = (payload: IMergeParties): AppThunk>> => ( dispatch ): Promise> => { diff --git a/services/common/src/redux/actionCreators/verifiableCredentialActionCreator.ts b/services/common/src/redux/actionCreators/verifiableCredentialActionCreator.ts index 544bd683a1..07260b3c76 100644 --- a/services/common/src/redux/actionCreators/verifiableCredentialActionCreator.ts +++ b/services/common/src/redux/actionCreators/verifiableCredentialActionCreator.ts @@ -93,3 +93,31 @@ export const fetchVCWalletInvitations = ( dispatch(hideLoading("modal")); }); }; + +export const deletePartyWalletConnection = ( + partyGuid: string +): AppThunk>> => ( + dispatch +): Promise> => { + dispatch(showLoading("modal")); + dispatch(request(reducerTypes.DELETE_VC_WALLET_CONNECTION)); + return CustomAxios() + .delete( + `${ENVIRONMENT.apiUrl}/verifiable-credentials/${partyGuid}/connection/`, + createRequestHeader() + ) + .then((response) => { + notification.success({ + message: "Digital Wallet Connection Deleted", + description: "The user may establish a new connection through minespace", + duration: 10, + }); + dispatch(success(reducerTypes.DELETE_VC_WALLET_CONNECTION)); + dispatch(hideLoading("modal")); + return response; + }) + .catch(() => { + dispatch(error(reducerTypes.DELETE_VC_WALLET_CONNECTION)); + dispatch(hideLoading("modal")); + }); +}; diff --git a/services/common/src/redux/selectors/projectSelectors.ts b/services/common/src/redux/selectors/projectSelectors.ts index 13a3473c5d..2d16c91a72 100644 --- a/services/common/src/redux/selectors/projectSelectors.ts +++ b/services/common/src/redux/selectors/projectSelectors.ts @@ -1,7 +1,7 @@ import { createSelector } from "reselect"; import { uniq } from "lodash"; import * as projectReducer from "../reducers/projectReducer"; -import { IParty, IProjectContact } from "../.."; +import { IParty, IProjectContact, IProjectSummaryDocument } from "../.."; import { getTransformedProjectSummaryAuthorizationTypes } from "./staticContentSelectors"; export const { @@ -26,6 +26,21 @@ const formatProjectSummaryParty = (party): IParty => { return { ...party, address: party.address[0] }; }; +const formatProjectSummaryDocuments = (documents = []): IProjectSummaryDocument[] => { + const allDocuments: any = { documents }; + const fieldNameMap = { + support_documents: "SPR", + spatial_documents: "SPT", + }; + Object.entries(fieldNameMap).forEach(([fieldName, docTypeCode]) => { + const matching = documents.filter( + (doc) => doc.project_summary_document_type_code === docTypeCode + ); + allDocuments[fieldName] = matching; + }); + return allDocuments; +}; + const formatProjectContact = (contacts): IProjectContact[] => { if (!contacts) { return contacts; @@ -78,6 +93,7 @@ export const getAmsAuthorizationTypes = createSelector( export const getFormattedProjectSummary = createSelector( [getProjectSummary, getProject, getAmsAuthorizationTypes], (summary, project, amsAuthTypes) => { + const documents = formatProjectSummaryDocuments(summary.documents); const contacts = formatProjectContact(project.contacts); const agent = formatProjectSummaryParty(summary.agent); const facility_operator = formatProjectSummaryParty(summary.facility_operator); @@ -90,6 +106,7 @@ export const getFormattedProjectSummary = createSelector( facility_operator, confirmation_of_submission, ...formatAuthorizations(amsAuthTypes, summary.status_code, summary.authorizations), + ...documents, }; formattedSummary.project_lead_party_guid = project.project_lead_party_guid; diff --git a/services/common/src/setupTests.ts b/services/common/src/setupTests.ts index b287af258b..8388132111 100644 --- a/services/common/src/setupTests.ts +++ b/services/common/src/setupTests.ts @@ -13,6 +13,14 @@ Enzyme.configure({ adapter: new Adapter() }); setTimeout(callback, 0); // eslint-disable-line @typescript-eslint/no-implied-eval }; +jest.mock("react", () => { + const original = jest.requireActual("react"); + return { + ...original, + useLayoutEffect: jest.fn(), + }; +}); + jest.mock("react-lottie", () => ({ __esModule: true, default: "lottie-mock", @@ -23,7 +31,7 @@ jest.mock("@mds/common/providers/featureFlags/useFeatureFlag", () => ({ isFeatureEnabled: () => true, }), })); - +window.scrollTo = jest.fn(); const location = JSON.stringify(window.location); delete window.location; diff --git a/services/core-api/app/api/parties/party/models/party.py b/services/core-api/app/api/parties/party/models/party.py index ac49b0fdb8..3c00d24c49 100644 --- a/services/core-api/app/api/parties/party/models/party.py +++ b/services/core-api/app/api/parties/party/models/party.py @@ -77,21 +77,23 @@ class Party(SoftDeleteMixin, AuditMixin, Base): uselist=False, remote_side=[party_guid], foreign_keys=[organization_guid]) - + digital_wallet_invitations = db.relationship( 'PartyVerifiableCredentialConnection', lazy='select', uselist=True, + primaryjoin= + "and_(PartyVerifiableCredentialConnection.party_guid == Party.party_guid, PartyVerifiableCredentialConnection.deleted_ind==False)", order_by='desc(PartyVerifiableCredentialConnection.update_timestamp)', overlaps='active_digital_wallet_connection') - + active_digital_wallet_connection = db.relationship( 'PartyVerifiableCredentialConnection', lazy='select', uselist=False, remote_side=[party_guid], primaryjoin= - 'and_(PartyVerifiableCredentialConnection.party_guid == Party.party_guid, PartyVerifiableCredentialConnection.connection_state==\'active\')', + 'and_(PartyVerifiableCredentialConnection.party_guid == Party.party_guid, PartyVerifiableCredentialConnection.deleted_ind==False, PartyVerifiableCredentialConnection.connection_state==\'active\')', overlaps='digital_wallet_invitations') @hybrid_property diff --git a/services/core-api/app/api/parties/party/models/party_orgbook_entity.py b/services/core-api/app/api/parties/party/models/party_orgbook_entity.py index 1249a897ae..e1bb8b65c5 100644 --- a/services/core-api/app/api/parties/party/models/party_orgbook_entity.py +++ b/services/core-api/app/api/parties/party/models/party_orgbook_entity.py @@ -23,7 +23,6 @@ class PartyOrgBookEntity(AuditMixin, Base): association_user = db.Column(db.String, nullable=False, default=User().get_user_username) association_timestamp = db.Column(db.DateTime, nullable=False, server_default=FetchedValue()) - def __repr__(self): return f'{self.__class__.__name__} {self.party_orgbook_entity_id}' @@ -48,4 +47,7 @@ def create(cls, registration_id, registration_status, registration_date, name_id party_guid=party_guid, company_alias=company_alias) party_orgbook_entity.save() - return party_orgbook_entity \ No newline at end of file + return party_orgbook_entity + + def delete(self, commit=True): + super(PartyOrgBookEntity, self).delete(commit) diff --git a/services/core-api/app/api/parties/party/resources/party_orgbook_entity_list_resource.py b/services/core-api/app/api/parties/party/resources/party_orgbook_entity_list_resource.py index d4d5c927a8..5b0ae02a6c 100644 --- a/services/core-api/app/api/parties/party/resources/party_orgbook_entity_list_resource.py +++ b/services/core-api/app/api/parties/party/resources/party_orgbook_entity_list_resource.py @@ -4,7 +4,7 @@ from werkzeug.exceptions import BadRequest, InternalServerError, NotFound, BadGateway from app.extensions import api -from app.api.utils.access_decorators import requires_role_edit_party +from app.api.utils.access_decorators import requires_role_edit_party, requires_role_mine_admin from app.api.utils.resources_mixins import UserMixin from app.api.utils.custom_reqparser import CustomReqparser from app.api.parties.party.models.party_orgbook_entity import PartyOrgBookEntity @@ -59,3 +59,14 @@ def post(self, party_guid): party.save() return party_orgbook_entity, 201 + + @api.doc(description='Delete a Party OrgBook Entity.') + @requires_role_mine_admin + @api.marshal_with(PARTY_ORGBOOK_ENTITY, code=204) + def delete(self, party_guid): + party_orgbook_entity = PartyOrgBookEntity.find_by_party_guid(party_guid) + if party_orgbook_entity is None: + raise NotFound('OrgBook entity not found.') + + party_orgbook_entity.delete() + return None, 204 diff --git a/services/core-api/app/api/projects/project_summary/models/project_summary.py b/services/core-api/app/api/projects/project_summary/models/project_summary.py index 92d362c2d3..9da11785db 100644 --- a/services/core-api/app/api/projects/project_summary/models/project_summary.py +++ b/services/core-api/app/api/projects/project_summary/models/project_summary.py @@ -191,11 +191,8 @@ def proponent_project_id(self): return None @classmethod - def find_by_project_summary_guid(cls, project_summary_guid, is_minespace_user=False): - if is_minespace_user: - return cls.query.filter_by( - project_summary_guid=project_summary_guid, deleted_ind=False).one_or_none() - return cls.query.filter(ProjectSummary.status_code.is_distinct_from("DFT")).filter_by( + def find_by_project_summary_guid(cls, project_summary_guid): + return cls.query.filter_by( project_summary_guid=project_summary_guid, deleted_ind=False).one_or_none() @classmethod @@ -203,11 +200,8 @@ def find_by_mine_guid(cls, mine_guid_to_search): return cls.query.filter(cls.mine_guid == mine_guid_to_search).all() @classmethod - def find_by_project_guid(cls, project_guid, is_minespace_user): - if is_minespace_user: - return cls.query.filter_by(project_guid=project_guid, deleted_ind=False).all() - return cls.query.filter(ProjectSummary.status_code.is_distinct_from("DFT")).filter_by( - project_guid=project_guid, deleted_ind=False).all() + def find_by_project_guid(cls, project_guid): + return cls.query.filter_by(project_guid=project_guid, deleted_ind=False).all() @classmethod def find_by_mine_document_guid(cls, mine_document_guid): diff --git a/services/core-api/app/api/projects/project_summary/resources/project_summary.py b/services/core-api/app/api/projects/project_summary/resources/project_summary.py index 8ee0cb24ce..5fa355bfc5 100644 --- a/services/core-api/app/api/projects/project_summary/resources/project_summary.py +++ b/services/core-api/app/api/projects/project_summary/resources/project_summary.py @@ -184,8 +184,7 @@ class ProjectSummaryResource(Resource, UserMixin): @requires_any_of([VIEW_ALL, MINESPACE_PROPONENT]) @api.marshal_with(PROJECT_SUMMARY_MODEL, code=200) def get(self, project_guid, project_summary_guid): - project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid, - is_minespace_user()) + project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid) if project_summary is None: raise NotFound('Project Description not found') @@ -200,8 +199,7 @@ def get(self, project_guid, project_summary_guid): @requires_any_of([MINE_ADMIN, MINESPACE_PROPONENT, EDIT_PROJECT_SUMMARIES]) @api.marshal_with(PROJECT_SUMMARY_MODEL, code=200) def put(self, project_guid, project_summary_guid): - project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid, - is_minespace_user()) + project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid) project = Project.find_by_project_guid(project_guid) data = self.parser.parse_args() @@ -283,8 +281,7 @@ def put(self, project_guid, project_summary_guid): @requires_any_of([MINE_ADMIN, MINESPACE_PROPONENT, EDIT_PROJECT_SUMMARIES]) @api.response(204, 'Successfully deleted.') def delete(self, project_guid, project_summary_guid): - project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid, - is_minespace_user()) + project_summary = ProjectSummary.find_by_project_summary_guid(project_summary_guid) if project_summary is None: raise NotFound('Project Description not found') diff --git a/services/core-api/app/api/projects/project_summary/resources/project_summary_list.py b/services/core-api/app/api/projects/project_summary/resources/project_summary_list.py index 2177281b3c..2fe0698adc 100644 --- a/services/core-api/app/api/projects/project_summary/resources/project_summary_list.py +++ b/services/core-api/app/api/projects/project_summary/resources/project_summary_list.py @@ -35,7 +35,7 @@ def get(self, project_guid): project_summaries = [] for project in projects: project_project_summaries = ProjectSummary.find_by_project_guid( - project.project_guid, is_minespace_user()) + project.project_guid) project_summaries = [*project_summaries, *project_project_summaries] return project_summaries diff --git a/services/core-api/app/api/services/traction_service.py b/services/core-api/app/api/services/traction_service.py index e91a5b571b..8122c8ae26 100644 --- a/services/core-api/app/api/services/traction_service.py +++ b/services/core-api/app/api/services/traction_service.py @@ -9,6 +9,7 @@ traction_token_url = Config.TRACTION_HOST + "/multitenancy/tenant/" + Config.TRACTION_TENANT_ID + "/token" traction_oob_create_invitation = Config.TRACTION_HOST + "/out-of-band/create-invitation" +traction_connections = Config.TRACTION_HOST + "/connections" traction_offer_credential = Config.TRACTION_HOST + "/issue-credential/send-offer" revoke_credential_url = Config.TRACTION_HOST + "/revocation/revoke" fetch_credential_exchanges = Config.TRACTION_HOST + "/issue-credential/records" @@ -84,6 +85,12 @@ def create_oob_connection_invitation(self, party: Party): return response + def delete_connection(self, connection_id) -> bool: + revoke_resp = requests.delete( + traction_connections + "/" + str(connection_id), headers=self.get_headers()) + assert revoke_resp.status_code == 200, f"revoke_resp={revoke_resp.json()}" + return True + def offer_mines_act_permit_111(self, connection_id, attributes): # https://github.com/bcgov/bc-vcpedia/blob/main/credentials/bc-mines-act-permit/1.1.1/governance.md#261-schema-definition payload = { diff --git a/services/core-api/app/api/verifiable_credentials/models/connection.py b/services/core-api/app/api/verifiable_credentials/models/connection.py index 01a43b339a..a4fa506a35 100644 --- a/services/core-api/app/api/verifiable_credentials/models/connection.py +++ b/services/core-api/app/api/verifiable_credentials/models/connection.py @@ -1,12 +1,11 @@ - from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.schema import FetchedValue from app.extensions import db -from app.api.utils.models_mixins import AuditMixin, Base +from app.api.utils.models_mixins import AuditMixin, Base, SoftDeleteMixin -class PartyVerifiableCredentialConnection(AuditMixin, Base): +class PartyVerifiableCredentialConnection(AuditMixin, SoftDeleteMixin, Base): """Verificable Credential reference to Traction, a Multi-tenant Hyperledger Aries Wallet""" __tablename__ = "party_verifiable_credential_connection" invitation_id = db.Column(db.String, primary_key=True) @@ -14,38 +13,40 @@ class PartyVerifiableCredentialConnection(AuditMixin, Base): party_guid = db.Column(UUID(as_uuid=True), db.ForeignKey('party.party_guid'), nullable=False) connection_id = db.Column(db.String) - connection_state = db.Column(db.String, server_default=FetchedValue()) + connection_state = db.Column(db.String, server_default=FetchedValue()) #ARIES-RFC 0023 https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange last_webhook_timestamp = db.Column(db.DateTime, nullable=True) - def __repr__(self): - return '' % (self.party_guid, self.connection_state) + return '' % ( + self.party_guid, self.connection_state) def json(self): return { - "connection_id":self.connection_id, - "connection_state":self.connection_state, + "connection_id": self.connection_id, + "connection_state": self.connection_state, "update_timestamp": self.update_timestamp, } @classmethod def find_by_party_guid(cls, party_guid) -> "PartyVerifiableCredentialConnection": - return cls.query.filter_by(party_guid=party_guid).all() - + return cls.query.filter_by(party_guid=party_guid, deleted_ind=False).all() + @classmethod def find_active_by_party_guid(cls, party_guid) -> "PartyVerifiableCredentialConnection": - return cls.query.filter_by(party_guid=party_guid, connection_state="active").first() - + return cls.query.filter_by( + party_guid=party_guid, connection_state="active", deleted_ind=False).first() + @classmethod def find_by_invitation_id(cls, invitation_id) -> "PartyVerifiableCredentialConnection": - return cls.query.filter_by(invitation_id=invitation_id).one_or_none() - + return cls.query.filter_by(invitation_id=invitation_id, deleted_ind=False).one_or_none() + @classmethod def find_by_connection_id(cls, connection_id) -> "PartyVerifiableCredentialConnection": - return cls.query.filter_by(connection_id=connection_id).one_or_none() - + return cls.query.filter_by(connection_id=connection_id, deleted_ind=False).one_or_none() + @classmethod def find_active_by_invitation_id(cls, invitation_id) -> "PartyVerifiableCredentialConnection": - return cls.query.filter_by(invitation_id=invitation_id, connection_state="active").one_or_none() - \ No newline at end of file + return cls.query.filter_by( + invitation_id=invitation_id, connection_state="active", + deleted_ind=False).one_or_none() diff --git a/services/core-api/app/api/verifiable_credentials/namespace.py b/services/core-api/app/api/verifiable_credentials/namespace.py index a20ef3a0b9..b39d519d7a 100644 --- a/services/core-api/app/api/verifiable_credentials/namespace.py +++ b/services/core-api/app/api/verifiable_credentials/namespace.py @@ -1,19 +1,20 @@ from flask_restx import Namespace -from app.api.verifiable_credentials.resources.verifiable_credential import VerifiableCredentialResource -from app.api.verifiable_credentials.resources.verifiable_credential_connections import VerifiableCredentialConnectionResource -from app.api.verifiable_credentials.resources.verifiable_credential_webhook import VerifiableCredentialWebhookResource -from app.api.verifiable_credentials.resources.verifiable_credential_map import VerifiableCredentialMinesActPermitResource -from app.api.verifiable_credentials.resources.verifiable_credential_map_detail import VerifiableCredentialCredentialExchangeResource -from app.api.verifiable_credentials.resources.verifiable_credential_revocation import VerifiableCredentialRevocationResource +from app.api.verifiable_credentials.resources.vc_connections import VerifiableCredentialConnectionResource +from app.api.verifiable_credentials.resources.vc_connection_invitations import VerifiableCredentialConnectionInvitationsResource +from app.api.verifiable_credentials.resources.traction_webhook import TractionWebhookResource +from app.api.verifiable_credentials.resources.vc_map import VerifiableCredentialMinesActPermitResource +from app.api.verifiable_credentials.resources.vc_map_detail import VerifiableCredentialCredentialExchangeResource +from app.api.verifiable_credentials.resources.vc_revocation import VerifiableCredentialRevocationResource from app.api.verifiable_credentials.resources.w3c_map_credential_resource import W3CCredentialResource, W3CCredentialListResource, W3CCredentialDeprecatedResource api = Namespace('verifiable-credentials', description='Variances actions/options') -api.add_resource(VerifiableCredentialResource, '') -api.add_resource(VerifiableCredentialWebhookResource, '/webhook/topic//') +api.add_resource(TractionWebhookResource, '/webhook/topic//') -api.add_resource(VerifiableCredentialConnectionResource, '//oob-invitation') +api.add_resource(VerifiableCredentialConnectionInvitationsResource, + '//oob-invitation') +api.add_resource(VerifiableCredentialConnectionResource, '//connection/') api.add_resource(VerifiableCredentialMinesActPermitResource, '//mines-act-permits') api.add_resource(VerifiableCredentialCredentialExchangeResource, diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_webhook.py b/services/core-api/app/api/verifiable_credentials/resources/traction_webhook.py similarity index 73% rename from services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_webhook.py rename to services/core-api/app/api/verifiable_credentials/resources/traction_webhook.py index 52f039f768..388680a242 100644 --- a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_webhook.py +++ b/services/core-api/app/api/verifiable_credentials/resources/traction_webhook.py @@ -23,16 +23,18 @@ PING = "ping" ISSUER_CREDENTIAL_REVOKED = "issuer_cred_rev" -class VerifiableCredentialWebhookResource(Resource, UserMixin): + +class TractionWebhookResource(Resource, UserMixin): + @api.doc(description='Endpoint to recieve webhooks from Traction.', params={}) def post(self, topic): if not is_feature_enabled(Feature.TRACTION_VERIFIABLE_CREDENTIALS): raise NotImplemented() - + #custom auth for traction if request.headers.get("x-api-key") != Config.TRACTION_WEBHOOK_X_API_KEY: - return Forbidden("bad x-api-key") - + return Forbidden("bad x-api-key") + webhook_body = request.get_json() current_app.logger.debug(f"webhook received : {webhook_body}") if "updated_at" not in webhook_body: @@ -42,7 +44,8 @@ def post(self, topic): if topic == CONNECTIONS: invitation_id = webhook_body['invitation_msg_id'] - vc_conn = PartyVerifiableCredentialConnection.query.unbound_unsafe().filter_by(invitation_id=invitation_id).first() + vc_conn = PartyVerifiableCredentialConnection.query.unbound_unsafe().filter_by( + invitation_id=invitation_id).first() assert vc_conn, f"connection.invitation_msg_id={invitation_id} not found. webhook_body={webhook_body}" if not vc_conn.connection_id: vc_conn.connection_id = webhook_body["connection_id"] @@ -52,55 +55,66 @@ def post(self, topic): # already processed a more recent webhook else: vc_conn.last_webhook_timestamp = webhook_timestamp - + new_state = webhook_body["state"] if new_state != vc_conn.connection_state and vc_conn.connection_state != DIDExchangeRequesterState.COMPLETED: # 'completed' is the final succesful state. - vc_conn.connection_state=new_state + vc_conn.connection_state = new_state vc_conn.save() - current_app.logger.info(f"Updated party_vc_conn connection_id={vc_conn.connection_id} with state={new_state}") + current_app.logger.info( + f"Updated party_vc_conn connection_id={vc_conn.connection_id} with state={new_state}" + ) if new_state == "deleted": # if deleted in the wallet (either in traction, or by the other agent) - vc_conn.connection_state=new_state - vc_conn.save() - current_app.logger.info(f"party_vc_conn connection_id={vc_conn.connection_id} was deleted") - + vc_conn.connection_state = new_state + vc_conn.save() + current_app.logger.info( + f"party_vc_conn connection_id={vc_conn.connection_id} was deleted") + elif topic == OUT_OF_BAND: - current_app.logger.info(f"out-of-band message invi_msg_id={webhook_body['invi_msg_id']}, state={webhook_body['state']}") - + current_app.logger.info( + f"out-of-band message invi_msg_id={webhook_body['invi_msg_id']}, state={webhook_body['state']}" + ) + elif topic == CREDENTIAL_OFFER: cred_exch_id = webhook_body["credential_exchange_id"] - cred_exch_record = PartyVerifiableCredentialMinesActPermit.query.unbound_unsafe().filter_by(cred_exch_id=cred_exch_id).first() + cred_exch_record = PartyVerifiableCredentialMinesActPermit.query.unbound_unsafe( + ).filter_by(cred_exch_id=cred_exch_id).first() assert cred_exch_record, f"issue_credential.credential_exchange_id={cred_exch_id} not found. webhook_body={webhook_body}" new_state = webhook_body["state"] if cred_exch_record.last_webhook_timestamp and cred_exch_record.last_webhook_timestamp >= webhook_timestamp: current_app.logger.warn(f"webhooks out of order catch, ignoring {webhook_body}") - # already processed a more recent webhook + # already processed a more recent webhook else: cred_exch_record.last_webhook_timestamp = webhook_timestamp if new_state == IssueCredentialIssuerState.ABANDONED: - current_app.logger.warning(f"cred_exch_id={cred_exch_id} is abanoned with message = {webhook_body['error_msg']}") + current_app.logger.warning( + f"cred_exch_id={cred_exch_id} is abanoned with message = {webhook_body['error_msg']}" + ) cred_exch_record.error_description = webhook_body['error_msg'] - - cred_exch_record.cred_exch_state=new_state + + cred_exch_record.cred_exch_state = new_state if new_state == IssueCredentialIssuerState.CREDENTIAL_ACKED: cred_exch_record.rev_reg_id = webhook_body["revoc_reg_id"] cred_exch_record.cred_rev_id = webhook_body["revocation_id"] cred_exch_record.save() - current_app.logger.info(f"Updated cred_exch_record cred_exch_id={cred_exch_id} with state={new_state}") + current_app.logger.info( + f"Updated cred_exch_record cred_exch_id={cred_exch_id} with state={new_state}") elif topic == ISSUER_CREDENTIAL_REVOKED: if webhook_body["state"] != "revoked": - current_app.logger.info(f"CREDENTIAL SUCCESSFULLY REVOKED received={request.get_json()}") - cred_exch = PartyVerifiableCredentialMinesActPermit.find_by_cred_exch_id(webhook_body["cred_ex_id"], unsafe=True) + current_app.logger.info( + f"CREDENTIAL SUCCESSFULLY REVOKED received={request.get_json()}") + cred_exch = PartyVerifiableCredentialMinesActPermit.find_by_cred_exch_id( + webhook_body["cred_ex_id"], unsafe=True) cred_exch.permit_amendment.permit.mines_act_permit_vc_locked = True cred_exch.save() - + elif topic == PING: current_app.logger.info(f"TrustPing received={request.get_json()}") - + else: - current_app.logger.info(f"unknown topic '{topic}', webhook_body={webhook_body}") + current_app.logger.info(f"unknown topic '{topic}', webhook_body={webhook_body}") diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_connections.py b/services/core-api/app/api/verifiable_credentials/resources/vc_connection_invitations.py similarity index 71% rename from services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_connections.py rename to services/core-api/app/api/verifiable_credentials/resources/vc_connection_invitations.py index 27dbce6884..99ebb79980 100644 --- a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_connections.py +++ b/services/core-api/app/api/verifiable_credentials/resources/vc_connection_invitations.py @@ -11,7 +11,9 @@ from app.api.utils.resources_mixins import UserMixin from app.api.utils.feature_flag import Feature, is_feature_enabled -class VerifiableCredentialConnectionResource(Resource, UserMixin): + +class VerifiableCredentialConnectionInvitationsResource(Resource, UserMixin): + @api.doc(description='Create a connection invitation for a party by guid', params={}) @requires_any_of([EDIT_PARTY, MINESPACE_PROPONENT]) def post(self, party_guid: str): @@ -20,12 +22,11 @@ def post(self, party_guid: str): party = Party.find_by_party_guid(party_guid) if not party: raise NotFound(f"party not found with party_guid {party_guid}") - + traction_svc = TractionService() invitation = traction_svc.create_oob_connection_invitation(party) - + return invitation - @api.doc(description='Create a connection invitation for a party by guid', params={}) @requires_any_of([EDIT_PARTY, MINESPACE_PROPONENT]) @@ -33,6 +34,17 @@ def post(self, party_guid: str): def get(self, party_guid: str): if not is_feature_enabled(Feature.TRACTION_VERIFIABLE_CREDENTIALS): raise NotImplemented() - party_vc_conn = PartyVerifiableCredentialConnection.find_by_party_guid(party_guid=party_guid) + party_vc_conn = PartyVerifiableCredentialConnection.find_by_party_guid( + party_guid=party_guid) return party_vc_conn - \ No newline at end of file + + @api.doc(description="Delete a connection for a party by guid", params={}) + @requires_any_of([EDIT_PARTY, MINESPACE_PROPONENT]) + def delete(self, party_guid): + if not is_feature_enabled(Feature.TRACTION_VERIFIABLE_CREDENTIALS): + raise NotImplemented() + party_vc_conn = PartyVerifiableCredentialConnection.find_by_party_guid(party_guid) + if not party_vc_conn: + raise NotFound(f"party_vc_conn not found with party_guid {party_guid}") + party_vc_conn.delete() + party_vc_conn.save() diff --git a/services/core-api/app/api/verifiable_credentials/resources/vc_connections.py b/services/core-api/app/api/verifiable_credentials/resources/vc_connections.py new file mode 100644 index 0000000000..d9730668dd --- /dev/null +++ b/services/core-api/app/api/verifiable_credentials/resources/vc_connections.py @@ -0,0 +1,34 @@ +from flask import current_app +from flask_restx import Resource +from werkzeug.exceptions import BadRequest +from app.extensions import api +from app.api.utils.access_decorators import requires_any_of, EDIT_PARTY, MINESPACE_PROPONENT + +from app.api.parties.party.models.party import Party +from app.api.verifiable_credentials.models.connection import PartyVerifiableCredentialConnection +from app.api.services.traction_service import TractionService +from app.api.verifiable_credentials.response_models import PARTY_VERIFIABLE_CREDENTIAL_CONNECTION +from app.api.utils.resources_mixins import UserMixin +from app.api.utils.feature_flag import Feature, is_feature_enabled + + +class VerifiableCredentialConnectionResource(Resource, UserMixin): + + @api.doc(description='Delete a connection party by connection_id', params={}) + @requires_any_of([EDIT_PARTY, MINESPACE_PROPONENT]) + def delete(self, party_guid: str): + + active_conn = PartyVerifiableCredentialConnection.find_active_by_party_guid(party_guid) + conns = PartyVerifiableCredentialConnection.find_by_party_guid(party_guid) + current_app.logger.warning(conns) + if not active_conn: + raise BadRequest(f"party has no active connection party_guid={party_guid}") + + # this will hard delete the didcomm connection, but will maintain the CORE records of anything that was exchanged on that connection + active_conn.delete() + + t_service = TractionService() + delete_success = t_service.delete_connection(active_conn.connection_id) + assert delete_success + + active_conn.save() diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_map.py b/services/core-api/app/api/verifiable_credentials/resources/vc_map.py similarity index 100% rename from services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_map.py rename to services/core-api/app/api/verifiable_credentials/resources/vc_map.py diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_map_detail.py b/services/core-api/app/api/verifiable_credentials/resources/vc_map_detail.py similarity index 100% rename from services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_map_detail.py rename to services/core-api/app/api/verifiable_credentials/resources/vc_map_detail.py diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_revocation.py b/services/core-api/app/api/verifiable_credentials/resources/vc_revocation.py similarity index 100% rename from services/core-api/app/api/verifiable_credentials/resources/verifiable_credential_revocation.py rename to services/core-api/app/api/verifiable_credentials/resources/vc_revocation.py diff --git a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential.py b/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential.py deleted file mode 100644 index 8cb79af296..0000000000 --- a/services/core-api/app/api/verifiable_credentials/resources/verifiable_credential.py +++ /dev/null @@ -1,14 +0,0 @@ -from flask_restx import Resource - -from app.extensions import api -from app.api.utils.access_decorators import requires_any_of, MINESPACE_PROPONENT, VIEW_ALL - -from app.api.utils.resources_mixins import UserMixin -from app.api.services.traction_service import TractionService - -class VerifiableCredentialResource(Resource, UserMixin): - @api.doc(description='test authorization with traction') - @requires_any_of([VIEW_ALL, MINESPACE_PROPONENT]) - def get(self): - traction_svc=TractionService() - return {"sample_token":traction_svc.token} diff --git a/services/core-api/tests/projects/project_summaries/models/test_project_summary_model.py b/services/core-api/tests/projects/project_summaries/models/test_project_summary_model.py index a79d746357..d951138098 100644 --- a/services/core-api/tests/projects/project_summaries/models/test_project_summary_model.py +++ b/services/core-api/tests/projects/project_summaries/models/test_project_summary_model.py @@ -6,14 +6,14 @@ def test_project_summary_find_by_project_summary_guid(db_session): project_summary = ProjectSummaryFactory() project_summary_guid = project_summary.project_summary_guid - project_summary = ProjectSummary.find_by_project_summary_guid(str(project_summary_guid), True) + project_summary = ProjectSummary.find_by_project_summary_guid(str(project_summary_guid)) assert project_summary.project_summary_guid == project_summary_guid def test_project_summary_find_by_project_guid(db_session): batch_size = 1 project = ProjectFactory.create_batch(size=batch_size) - project_summaries = ProjectSummary.find_by_project_guid(str(project[0].project_guid), True) + project_summaries = ProjectSummary.find_by_project_guid(str(project[0].project_guid)) assert len(project_summaries) == batch_size assert all(project_summary.project_guid == project[0].project_guid diff --git a/services/core-web/cypress/e2e/createproject.cy.ts b/services/core-web/cypress/e2e/createproject.cy.ts index 9285549ec1..93d93419e0 100644 --- a/services/core-web/cypress/e2e/createproject.cy.ts +++ b/services/core-web/cypress/e2e/createproject.cy.ts @@ -35,17 +35,142 @@ describe("Major Projects", () => { cy.get("#project_summary_description").type("This is just a Cypress test project description", { force: true, }); - cy.get("#contacts\\[0\\]\\.first_name").type("Cypress", { force: true }); - cy.get("#contacts\\[0\\]\\.last_name").type("Test", { force: true }); - cy.get("#contacts\\[0\\]\\.email").type("cypress@mds.com", { force: true }); - cy.get("#contacts\\[0\\]\\.phone_number").type("1234567890", { force: true }); + + // SAVE & CONTINUE - skip to Purpose & Authorization + cy.contains("Save & Continue").click({ force: true }); + cy.contains("Project Lead", { timeout: 10000 }); + cy.contains("Purpose and Authorization", { timeout: 10000 }).click({ force: true }); + cy.contains("Regulatory Approval Type", { timeout: 10000 }); + cy.get('[data-cy="checkbox-authorization-OTHER"]').click({ force: true }); cy.get( '[name="authorizations.OTHER[0].authorization_description"]' ).type("legislation description", { force: true }); + // SAVE & CONTINUE - direct to Project Contacts + cy.contains("Save & Continue").click({ force: true }); + cy.contains("First Name", { timeout: 10000 }); + + cy.get(`[name="contacts[0].first_name"]`).type("Cypress", { force: true }); + cy.get(`[name="contacts[0].last_name"]`).type("Test", { force: true }); + cy.get(`[name="contacts[0].email"]`).type("cypress@mds.com", { force: true }); + cy.get(`[name="contacts[0].phone_number"]`).type("1234567890", { force: true }); + cy.get(`[name="contacts[0].address.address_line_1"]`).type("123 Fake St", { force: true }); + cy.contains("Please select") + .first() + .click({ + force: true, + }); + cy.get('[title="Canada"]').click({ force: true }); + cy.get(`[name="contacts[0].address.city"]`).type("Cityville", { force: true }); + cy.contains("Please select") + .first() + .click({ + force: true, + }); + cy.get('[title="AB"]').click({ force: true }); + cy.get(`[name="contacts[0].address.post_code"]`).type("A0A0A0", { force: true }); + + // SAVE & CONTINUE - Applicant Information + cy.contains("Save & Continue").click({ force: true }); + cy.contains("Applicant Information", { timeout: 10000 }); + + cy.contains("Individual").click({ force: true }); + cy.get(`[name="applicant.first_name"]`).type("Cypress", { force: true }); + cy.get(`[name="applicant.party_name"]`).type("Test", { force: true }); + cy.get(`[name="applicant.phone_no"]`).type("1231231234", { force: true }); + cy.get(`[name="applicant.email"]`).type("email@email.com", { force: true }); + cy.get(`[name="applicant.address[0].address_line_1"]`).type("123 Fake St", { force: true }); + cy.get(`[data-cy="applicant.address[0].address_type_code"]`) + .contains("Please select") + .click({ + force: true, + }); + cy.get('[title="Canada"]').click({ force: true }); + cy.get(`[data-cy="applicant.address[0].sub_division_code"]`) + .contains("Please select") + .click({ + force: true, + }); + cy.get('[title="AB"]').click({ force: true }); + cy.get(`[name="applicant.address[0].post_code"]`).type("A0A0A0", { force: true }); + cy.get(`[name="applicant.address[0].city"]`).type("Cityville", { force: true }); + cy.contains("Same as mailing address") + .first() + .click({ force: true }); + cy.contains("Same as legal address") + .first() + .click({ force: true }); + + // SAVE & CONTINUE - Agent + cy.contains("Save & Continue").click({ force: true }); + cy.contains("Are you an agent applying on behalf of the applicant?", { timeout: 10000 }); + + cy.contains("No").click({ force: true }); + + // SAVE & CONTINUE - Location, Access and Land Use + cy.contains("Save & Continue").click({ force: true }); + cy.scrollTo(0, 0); + cy.get(`[name="is_legal_land_owner"]`, { timeout: 10000 }) + .first() + .scrollIntoView() + .click({ force: true }); // click yes + cy.get(`[name="facility_latitude"]`).type("48", { force: true }); + cy.get(`[name="facility_longitude"]`).type("-114", { force: true }); + + cy.get(`[data-cy="facility_coords_source"]`) + .contains("Please select") + .click({ force: true }); + cy.get('[title="GPS"]').click({ force: true }); + cy.get(`[data-cy="nearest_municipality"]`) + .contains("Please select") + .click({ force: true }); + cy.get('[title="Abbotsford"]').click({ force: true }); + + cy.get(`[name="facility_pid_pin_crown_file_no"]`).type("123", { force: true }); + cy.get(`[name="facility_lease_no"]`).type("456", { force: true }); + + // SAVE & CONTINUE - Mine Components and Offsite Infrastructure + cy.contains("Save & Continue").click({ force: true }); + cy.contains("Facility Type", { timeout: 10000 }); + cy.get(`[name="facility_type"]`).type("facility type", { force: true }); + cy.get(`[name="facility_desc"]`).type("facility description", { force: true }); + + cy.get(`[data-cy="regional_district_id"]`) + .contains("Please select") + .click({ force: true }); + cy.get('[title="Cariboo"]').click({ force: true }); + + cy.get(`[name="facility_operator.address.address_line_1"]`).type("123 Fake St", { + force: true, + }); + cy.get(`[name="facility_operator.address.city"]`).type("Cityville", { force: true }); + + cy.get(`[data-cy="facility_operator.address.sub_division_code"]`) + .contains("Please select") + .click({ + force: true, + }); + cy.get('[title="AB"]').click({ force: true }); + cy.get(`[name="zoning"]`, { timeout: 10000 }) + .first() + .click(); // click yes + + cy.get(`[name="facility_operator.first_name"]`).type("Firstname", { force: true }); + cy.get(`[name="facility_operator.party_name"]`).type("Lastname", { force: true }); + cy.get(`[name="facility_operator.phone_no"]`).type("1231231234", { force: true }); + + // SAVE & CONTINUE - skip to Declaration + cy.contains("Save & Continue").click({ force: true }); + cy.get("#expected_draft_irt_submission_date", { timeout: 10000 }); + cy.contains("Declaration", { timeout: 10000 }).click({ force: true }); + + cy.get("input#ADD_EDIT_PROJECT_SUMMARY_confirmation_of_submission", { timeout: 10000 }).click({ + force: true, + }); + // Submit the project - cy.get('[data-cy="project-summary-submit-button"]').click({ force: true }); + cy.contains("Submit").click({ force: true }); // wait for API to respond before navigating cy.wait(15000); // Navigate back to projects diff --git a/services/core-web/cypress/e2e/majorprojects.cy.ts b/services/core-web/cypress/e2e/majorprojects.cy.ts index 95d1dbba51..57a6c8581b 100644 --- a/services/core-web/cypress/e2e/majorprojects.cy.ts +++ b/services/core-web/cypress/e2e/majorprojects.cy.ts @@ -18,9 +18,9 @@ describe("Major Projects", () => { it("should upload and download a document successfully", () => { const fileName = "dummy.pdf"; - cy.get("#project-summary-submit").then(($button) => { - $button[0].click(); - }); + cy.contains("Document Upload").click(); + + cy.contains("Edit Project Description").click(); cy.fixture(fileName).then((fileContent) => { // Intercept the POST request and stub the response @@ -63,11 +63,13 @@ describe("Major Projects", () => { } ).as("statusRequest"); - cy.get('input[type="file"]').attachFile({ - fileContent: fileContent, - fileName: fileName, - mimeType: "application/pdf", - }); + cy.get('input[type="file"]') + .eq(1) + .attachFile({ + fileContent: fileContent, + fileName: fileName, + mimeType: "application/pdf", + }); // Wait for the upload request to complete(simulated) cy.wait("@uploadRequest").then((interception) => { diff --git a/services/core-web/src/components/Forms/parties/PartyOrgBookForm.tsx b/services/core-web/src/components/Forms/parties/PartyOrgBookForm.tsx index 2dbb69ce17..4fd1144824 100644 --- a/services/core-web/src/components/Forms/parties/PartyOrgBookForm.tsx +++ b/services/core-web/src/components/Forms/parties/PartyOrgBookForm.tsx @@ -6,11 +6,14 @@ import { isEmpty } from "lodash"; import { useDispatch } from "react-redux"; import { createPartyOrgBookEntity, + deletePartyOrgBookEntity, fetchPartyById, } from "@mds/common/redux/actionCreators/partiesActionCreator"; import { ORGBOOK_ENTITY_URL } from "@/constants/routes"; import { IOrgbookCredential, IParty } from "@mds/common"; import OrgBookSearch from "@mds/common/components/parties/OrgBookSearch"; +import * as Permission from "@/constants/permissions"; +import AuthorizationWrapper from "@/components/common/wrappers/AuthorizationWrapper"; interface PartyOrgBookFormProps { party: IParty; @@ -20,6 +23,8 @@ export const PartyOrgBookForm: FC = ({ party }) => { const [isAssociating, setIsAssociating] = useState(false); const dispatch = useDispatch(); const [credential, setCredential] = useState(null); + const [currentParty, setCurrentParty] = useState(party.party_orgbook_entity.name_text); + const [isAssociated, setIsAssociated] = useState(!!party.party_orgbook_entity.name_text); const handleAssociateButtonClick = async () => { setIsAssociating(true); @@ -31,6 +36,17 @@ export const PartyOrgBookForm: FC = ({ party }) => { await dispatch(fetchPartyById(party.party_guid)); setIsAssociating(false); + setIsAssociated(true); + }; + + const handleDisassociateButtonClick = async () => { + setIsAssociating(true); + + await dispatch(deletePartyOrgBookEntity(party.party_guid)); + await dispatch(fetchPartyById(party.party_guid)); + setIsAssociating(false); + setCurrentParty(""); + setIsAssociated(false); }; const hasOrgBookCredential = !isEmpty(credential); @@ -38,7 +54,11 @@ export const PartyOrgBookForm: FC = ({ party }) => { return ( - + - + + + ); diff --git a/services/core-web/src/components/Forms/projectSummaries/ProjectSummaryForm.tsx b/services/core-web/src/components/Forms/projectSummaries/ProjectSummaryForm.tsx deleted file mode 100644 index cb8ba551dd..0000000000 --- a/services/core-web/src/components/Forms/projectSummaries/ProjectSummaryForm.tsx +++ /dev/null @@ -1,551 +0,0 @@ -import React, { FC, useState } from "react"; -import { compose } from "redux"; -import { connect, useSelector } from "react-redux"; -import { useHistory, useParams, withRouter } from "react-router-dom"; -import { - FieldArray, - Field, - reduxForm, - formValueSelector, - InjectedFormProps, - change, -} from "redux-form"; -import { Alert, Button, Row, Col, Typography, Popconfirm, Form } from "antd"; -import { DeleteOutlined, PlusOutlined } from "@ant-design/icons"; -import { - maxLength, - phoneNumber, - required, - email, - dateNotBeforeOther, - dateNotAfterOther, -} from "@common/utils/Validate"; -import { - getDropdownProjectSummaryStatusCodes, - getProjectSummaryDocumentTypesHash, -} from "@mds/common/redux/selectors/staticContentSelectors"; -import AuthorizationsInvolved from "@mds/common/components/projectSummary/AuthorizationsInvolved"; -import { getDropdownProjectLeads } from "@mds/common/redux/selectors/partiesSelectors"; -import { getUserAccessData } from "@mds/common/redux/selectors/authenticationSelectors"; -import { Feature, IGroupedDropdownList, IProject, IProjectSummary, USER_ROLES } from "@mds/common"; -import { normalizePhone } from "@common/utils/helpers"; -import * as FORM from "@/constants/forms"; -import * as routes from "@/constants/routes"; -import { renderConfig } from "@/components/common/config"; -import LinkButton from "@/components/common/buttons/LinkButton"; -import { ProjectSummaryDocumentUpload } from "@/components/Forms/projectSummaries/ProjectSummaryDocumentUpload"; -import ArchivedDocumentsSection from "@common/components/documents/ArchivedDocumentsSection"; -import { MajorMineApplicationDocument } from "@mds/common/models/documents/document"; -import ProjectLinks from "@mds/common/components/projects/ProjectLinks"; -import { useFeatureFlag } from "@mds/common/providers/featureFlags/useFeatureFlag"; - -interface ProjectSummaryFormProps { - project: IProject; - initialValues: Partial; - onSubmit: any; - handleSaveData: (message: string) => Promise; - removeDocument: (event, documentGuid: string) => Promise; - archivedDocuments: MajorMineApplicationDocument[]; - onArchivedDocuments: (mineGuid, projectSummaryGuid) => Promise; - isNewProject: boolean; -} - -const unassignedProjectLeadEntry = { - label: "Unassigned", - value: null, -}; - -const contactFields = ({ fields, isNewProject, isEditMode }) => { - return ( - <> - {fields.map((field, index) => { - return ( -
- {index === 0 ? ( -

Primary project contact

- ) : ( - <> - - -

Additional project contact #{index}

- - - {!isNewProject && ( - fields.remove(index)} - okText="Remove" - cancelText="Cancel" - > - - - )} - -
- - )} - - - - - - - - - - - - - - - - - - - - - - - - - {index === 0 &&

Additional project contacts (optional)

} -
- ); - })} - {(isNewProject || isEditMode) && ( - { - fields.push({ is_primary: false }); - }} - title="Add additional project contacts" - > - Add additional project contacts - - )} - - ); -}; - -const ProjectSummaryForm: FC & ProjectSummaryFormProps> = ( - props -) => { - const { isFeatureEnabled } = useFeatureFlag(); - const majorProjectsFeatureEnabled = isFeatureEnabled(Feature.MAJOR_PROJECT_LINK_PROJECTS); - const [isEditMode, setIsEditMode] = useState(false); - const projectLeads: IGroupedDropdownList = useSelector(getDropdownProjectLeads); - const userRoles: string[] = useSelector(getUserAccessData); - const formSelector = formValueSelector(FORM.ADD_EDIT_PROJECT_SUMMARY); - const expected_draft_irt_submission_date = useSelector((state) => - formSelector(state, "expected_draft_irt_submission_date") - ); - const expected_permit_application_date = useSelector((state) => - formSelector(state, "expected_permit_application_date") - ); - const expected_permit_receipt_date = useSelector((state) => - formSelector(state, "expected_permit_receipt_date") - ); - const documents = useSelector((state) => formSelector(state, "documents")); - - const projectSummaryStatusCodes = useSelector(getDropdownProjectSummaryStatusCodes); - const projectSummaryDocumentTypesHash = useSelector(getProjectSummaryDocumentTypesHash); - - const projectLeadData = [unassignedProjectLeadEntry, ...projectLeads[0]?.opt]; - const { mineGuid } = useParams<{ mineGuid: string }>(); - const history = useHistory(); - - const renderProjectDetails = () => { - const { - project: { project_lead_party_guid }, - } = props; - return ( -
- {!props.isNewProject && !project_lead_party_guid && ( - - Please assign a Project Lead to this project via the{" "} - Project contacts section. -

- } - type="warning" - showIcon - /> - )} -
- -
- Project details -
- - {props.initialValues?.status_code && ( - - - value !== "DFT")} - disabled={!isEditMode} - /> - - - )} - - - - - Proponent project tracking ID (optional) -
- - If your company uses a tracking number to identify projects, please provide it - here. - - - } - component={renderConfig.FIELD} - validate={[maxLength(20)]} - disabled={!props.isNewProject && !isEditMode} - /> - - Project Overview -
- - Provide a 2-3 paragraph high-level description of your proposed project. - - - } - component={renderConfig.AUTO_SIZE_FIELD} - minRows={10} - validate={[maxLength(4000), required]} - disabled={!props.isNewProject && !isEditMode} - /> - - {majorProjectsFeatureEnabled && ( - - - routes.PRE_APPLICATIONS.dynamicRoute(p.project_guid, p.project_summary_guid) - } - /> - - )} -
-
- ); - }; - - const renderContacts = () => { - return ( -
- Project contacts -

EMLI contacts

- - - Project Lead

} - component={renderConfig.SELECT} - data={projectLeadData} - disabled={!props.isNewProject && !isEditMode} - /> - -
-

Proponent contacts

- <> - - -
- ); - }; - - const renderProjectDates = () => { - return ( -
- Project dates - -
- - - - - - - - -
- ); - }; - - const renderDocuments = () => { - const canRemoveDocuments = - userRoles.includes(USER_ROLES.role_admin) || - userRoles.includes(USER_ROLES.role_edit_project_summaries); - return ( -
- -
- ); - }; - - const renderArchivedDocuments = () => { - return ; - }; - - const cancelEdit = () => { - props.reset(); - setIsEditMode(false); - }; - - const toggleEditMode = () => { - setIsEditMode(!isEditMode); - }; - - return ( -
{ - const message = props.isNewProject - ? "Successfully submitted a project description to the Province of British Columbia." - : "Successfully updated the project."; - props.handleSaveData(message); - }} - > -
- {!props.isNewProject && !isEditMode && ( - <> - - - )} - {(props.isNewProject || isEditMode) && ( - <> - { - if (props.isNewProject) { - const url = routes.MINE_PRE_APPLICATIONS.dynamicRoute(mineGuid); - history.push(url); - } else if (isEditMode) { - cancelEdit(); - } - }} - okText="Yes" - cancelText="No" - > - - - - - )} -
- {renderProjectDetails()} -
- -
- {renderProjectDates()} -
- {renderContacts()} -
- {renderDocuments()} -
- {renderArchivedDocuments()} -
- {(props.isNewProject || isEditMode) && ( - <> - { - if (props.isNewProject) { - const url = routes.MINE_PRE_APPLICATIONS.dynamicRoute(mineGuid); - history.push(url); - } else if (isEditMode) { - cancelEdit(); - } - }} - okText="Yes" - cancelText="No" - > - - - - - )} -
- - ); -}; - -const mapDispatchToProps = { - change, -}; - -export default (compose( - withRouter, - connect(null, mapDispatchToProps), - reduxForm({ - form: FORM.ADD_EDIT_PROJECT_SUMMARY, - enableReinitialize: true, - touchOnBlur: true, - touchOnChange: false, - }) -)(ProjectSummaryForm) as any) as FC; diff --git a/services/core-web/src/components/dashboard/majorProjectHomePage/MajorProjectTable.js b/services/core-web/src/components/dashboard/majorProjectHomePage/MajorProjectTable.js index 6d80d61a2b..b7e2914e5b 100644 --- a/services/core-web/src/components/dashboard/majorProjectHomePage/MajorProjectTable.js +++ b/services/core-web/src/components/dashboard/majorProjectHomePage/MajorProjectTable.js @@ -86,7 +86,7 @@ export const MajorProjectTable = (props) => { sortField: "project_title", sorter: true, render: (text, record) => ( - + {text} ), @@ -130,7 +130,7 @@ export const MajorProjectTable = (props) => {
- + diff --git a/services/core-web/src/components/mine/Projects/InformationRequirementsTableTab.js b/services/core-web/src/components/mine/Projects/InformationRequirementsTableTab.js index d4ebddf927..fd3fd41413 100644 --- a/services/core-web/src/components/mine/Projects/InformationRequirementsTableTab.js +++ b/services/core-web/src/components/mine/Projects/InformationRequirementsTableTab.js @@ -195,7 +195,7 @@ export class InformationRequirementsTableTab extends Component { - + Back to: {mine_name} Project overview diff --git a/services/core-web/src/components/mine/Projects/MineProjectTable.js b/services/core-web/src/components/mine/Projects/MineProjectTable.js index acba62ba23..9ba845221b 100644 --- a/services/core-web/src/components/mine/Projects/MineProjectTable.js +++ b/services/core-web/src/components/mine/Projects/MineProjectTable.js @@ -91,7 +91,7 @@ export const MineProjectTable = (props) => {
- + diff --git a/services/core-web/src/components/mine/Projects/Project.js b/services/core-web/src/components/mine/Projects/Project.js index 414f7cf411..fefdfa5c70 100644 --- a/services/core-web/src/components/mine/Projects/Project.js +++ b/services/core-web/src/components/mine/Projects/Project.js @@ -89,10 +89,10 @@ export class Project extends Component { project_guid, information_requirements_table: { irt_guid }, } = this.props.project; - let url = routes.PROJECTS.dynamicRoute(project_guid); + let url = routes.EDIT_PROJECT.dynamicRoute(project_guid); switch (activeTab) { case "overview": - url = routes.PROJECTS.dynamicRoute(project_guid); + url = routes.EDIT_PROJECT.dynamicRoute(project_guid); break; case "intro-project-overview": url = routes.INFORMATION_REQUIREMENTS_TABLE.dynamicRoute(project_guid, irt_guid); @@ -107,7 +107,7 @@ export class Project extends Component { url = routes.PROJECT_DECISION_PACKAGE.dynamicRoute(project_guid); break; default: - url = routes.PROJECTS.dynamicRoute(project_guid); + url = routes.EDIT_PROJECT.dynamicRoute(project_guid); } return this.props.history.replace(url); }; diff --git a/services/core-web/src/components/mine/Projects/ProjectOverviewTab.js b/services/core-web/src/components/mine/Projects/ProjectOverviewTab.js index f60d31eb38..e06ec554ce 100644 --- a/services/core-web/src/components/mine/Projects/ProjectOverviewTab.js +++ b/services/core-web/src/components/mine/Projects/ProjectOverviewTab.js @@ -18,7 +18,7 @@ import CustomPropTypes from "@/customPropTypes"; import ProjectStagesTable from "./ProjectStagesTable"; import withFeatureFlag from "@mds/common/providers/featureFlags/withFeatureFlag"; import { Feature } from "@mds/common"; -import ProjectLinks from "@mds/common/components/projects/ProjectLinks"; +import ProjectLinks from "@mds/common/components/projectSummary/ProjectLinks"; const propTypes = { informationRequirementsTableStatusCodesHash: PropTypes.objectOf(PropTypes.string).isRequired, @@ -138,7 +138,7 @@ export class ProjectOverviewTab extends Component { link: ( + + +
+ + +
-
); diff --git a/services/core-web/src/components/navigation/NotificationDrawer.tsx b/services/core-web/src/components/navigation/NotificationDrawer.tsx index 1e28e312ed..be3a891bf1 100644 --- a/services/core-web/src/components/navigation/NotificationDrawer.tsx +++ b/services/core-web/src/components/navigation/NotificationDrawer.tsx @@ -16,8 +16,8 @@ import { INFORMATION_REQUIREMENTS_TABLE, MINE_TAILINGS_DETAILS, NOTICE_OF_DEPARTURE, - PRE_APPLICATIONS, - PROJECTS, + EDIT_PROJECT_SUMMARY, + EDIT_PROJECT, VIEW_MINE_INCIDENT, PROJECT_DOCUMENT_MANAGEMENT, REPORT_VIEW_EDIT, @@ -116,7 +116,7 @@ const NotificationDrawer: FC = (props) => { notification.notification_document.metadata.entity_guid ); case "ProjectSummary": - return PRE_APPLICATIONS.dynamicRoute( + return EDIT_PROJECT_SUMMARY.dynamicRoute( notification.notification_document.metadata.project.project_guid, notification.notification_document.metadata.entity_guid ); @@ -126,7 +126,7 @@ const NotificationDrawer: FC = (props) => { notification.notification_document.metadata.entity_guid ); case "MajorMineApplication": - return PROJECTS.dynamicRoute( + return EDIT_PROJECT.dynamicRoute( notification.notification_document.metadata.project.project_guid, "final-app" ); diff --git a/services/core-web/src/components/parties/PartyProfile.js b/services/core-web/src/components/parties/PartyProfile.js index 0121af5c54..6cf1acd687 100644 --- a/services/core-web/src/components/parties/PartyProfile.js +++ b/services/core-web/src/components/parties/PartyProfile.js @@ -32,6 +32,7 @@ import { getPartyRelationshipTypeHash, getPartyBusinessRoleOptionsHash, } from "@mds/common/redux/selectors/staticContentSelectors"; +import { deletePartyWalletConnection } from "@mds/common/redux/actionCreators/verifiableCredentialActionCreator"; import { formatDate, dateSorter } from "@common/utils/helpers"; import * as Strings from "@mds/common/constants/strings"; import { EDIT } from "@/constants/assets"; @@ -56,6 +57,7 @@ const propTypes = { history: PropTypes.shape({ push: PropTypes.func }).isRequired, updateParty: PropTypes.func.isRequired, deleteParty: PropTypes.func.isRequired, + deletePartyWalletConnection: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired, parties: PropTypes.arrayOf(CustomPropTypes.party).isRequired, @@ -130,6 +132,15 @@ export class PartyProfile extends Component { .finally(() => this.setState({ deletingParty: false })); }; + deletePartyWalletConnection = () => { + const { id } = this.props.match.params; + this.setState({ deletingParty: true }); + this.props + .deletePartyWalletConnection(id) + .then(this.props.fetchPartyById(id)) + .finally(() => this.setState({ deletingParty: false })); + }; + render() { const { id } = this.props.match.params; const party = this.props.parties[id]; @@ -347,13 +358,35 @@ export class PartyProfile extends Component { )}
- {isFeatureEnabled(Feature.VERIFIABLE_CREDENTIALS) && - party.party_type_code === "ORG" && ( + {isFeatureEnabled(Feature.VERIFIABLE_CREDENTIALS) && party.party_type_code === "ORG" && ( + <>
Digital Wallet Connection Status:{" "} {VC_CONNECTION_STATES[party?.digital_wallet_connection_status]} + {VC_CONNECTION_STATES[party?.digital_wallet_connection_status] === "Active" && ( + +

+ Are you sure you want to delete the digitial wallet connection for + ' + {party.name}'? This is irreversable and destructive. +

+
+ } + onConfirm={this.deletePartyWalletConnection} + okText="Yes" + cancelText="No" + > + + + )}
- )} + + )}
fetchPartyById, fetchMineBasicInfoList, deleteParty, + deletePartyWalletConnection, updateParty, openModal, closeModal, diff --git a/services/core-web/src/constants/routes.ts b/services/core-web/src/constants/routes.ts index 85816fa811..eb20817267 100644 --- a/services/core-web/src/constants/routes.ts +++ b/services/core-web/src/constants/routes.ts @@ -195,21 +195,27 @@ export const MINE_PRE_APPLICATIONS = { }; export const ADD_PROJECT_SUMMARY = { - route: "/mines/:mineGuid/project-description/new", - dynamicRoute: (mineGuid) => `/mines/${mineGuid}/project-description/new`, + route: "/mines/:mineGuid/project-description/new/:tab", + dynamicRoute: (mineGuid, tab = "basic-information") => + `/mines/${mineGuid}/project-description/new/${tab}`, component: ProjectSummary, }; -export const PRE_APPLICATIONS = { - route: "/pre-applications/:projectGuid/project-description/:projectSummaryGuid", - dynamicRoute: (projectGuid, projectSummaryGuid) => - `/pre-applications/${projectGuid}/project-description/${projectSummaryGuid}`, - hashRoute: (projectGuid, projectSummaryGuid, link) => - `/pre-applications/${projectGuid}/project-description/${projectSummaryGuid}/${link}`, +export const EDIT_PROJECT_SUMMARY = { + route: "/pre-applications/:projectGuid/project-description/:projectSummaryGuid/:mode/:tab", + dynamicRoute: ( + projectGuid, + projectSummaryGuid, + activeTab = "basic-information", + viewMode = true + ) => + `/pre-applications/${projectGuid}/project-description/${projectSummaryGuid}/${ + viewMode ? "view" : "edit" + }/${activeTab}`, component: ProjectSummary, }; -export const PROJECTS = { +export const EDIT_PROJECT = { route: "/pre-applications/:projectGuid/:tab", dynamicRoute: (projectGuid, tab = "overview") => `/pre-applications/${projectGuid}/${tab}`, component: Project, diff --git a/services/core-web/src/routes/MineDashboardRoutes.js b/services/core-web/src/routes/MineDashboardRoutes.js index ed4e880052..ddd5035c21 100644 --- a/services/core-web/src/routes/MineDashboardRoutes.js +++ b/services/core-web/src/routes/MineDashboardRoutes.js @@ -27,15 +27,15 @@ const MineDashboardRoutes = () => ( /> - + ({ }), })); +window.scrollTo = jest.fn(); const location = JSON.stringify(window.location); delete window.location; diff --git a/services/core-web/src/styles/components/SteppedForm.scss b/services/core-web/src/styles/components/SteppedForm.scss index 34d7506fc8..a0fd938756 100644 --- a/services/core-web/src/styles/components/SteppedForm.scss +++ b/services/core-web/src/styles/components/SteppedForm.scss @@ -15,7 +15,7 @@ } .stepped-form>.ant-menu-item-selected { - border-left: 4px solid $gov-blue; + border-left: 4px solid $violet; background-color: #FFFFFF !important; border-right: unset !important; } diff --git a/services/core-web/src/tests/components/Forms/projectSummaries/ProjectSummaryForm.spec.tsx b/services/core-web/src/tests/components/Forms/projectSummaries/ProjectSummaryForm.spec.tsx deleted file mode 100644 index 59414c6448..0000000000 --- a/services/core-web/src/tests/components/Forms/projectSummaries/ProjectSummaryForm.spec.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { render } from "@testing-library/react"; -import ProjectSummaryForm from "@/components/Forms/projectSummaries/ProjectSummaryForm"; -import * as MOCK from "@/tests/mocks/dataMocks"; -import { store } from "@/App"; -import { Provider } from "react-redux"; -import { BrowserRouter } from "react-router-dom"; - -const props: any = {}; - -const setupProps = () => { - props.handleSubmit = jest.fn(); - props.submitting = false; - props.formValues = { contacts: [{}] }; - props.initialValues = { - projectSummaryAuthorizationTypes: [], - authorizations: [], - }; - props.projectSummary = { documents: [], contacts: [{}] }; - props.projectSummaryDocumentTypesHash = {}; - props.projectSummaryPermitTypesHash = {}; - props.projectSummaryAuthorizationTypesHash = {}; - props.project = MOCK.PROJECT; - props.projectLeads = [ - { groupName: "Active", opt: [] }, - { groupName: "Inactive", opt: [] }, - ]; - props.projectSummaryStatusCodes = []; - props.userRoles = []; -}; - -beforeEach(() => { - setupProps(); -}); - -describe("ProjectSummaryForm", () => { - it("renders properly", () => { - const { container } = render( - - - - - - ); - expect(container.firstChild).toMatchSnapshot(); - }); -}); diff --git a/services/core-web/src/tests/components/Forms/projectSummaries/__snapshots__/ProjectSummaryForm.spec.tsx.snap b/services/core-web/src/tests/components/Forms/projectSummaries/__snapshots__/ProjectSummaryForm.spec.tsx.snap deleted file mode 100644 index 0ea22be7d6..0000000000 --- a/services/core-web/src/tests/components/Forms/projectSummaries/__snapshots__/ProjectSummaryForm.spec.tsx.snap +++ /dev/null @@ -1,1509 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ProjectSummaryForm renders properly 1`] = ` -
-
- -
-
- -
-
-

- Project details -

-
-
-
-
-
- -
-
-
- -
- - - -
-
-
-
-
-
-
- -
-
-
- -
- - - -
-
-
-
-
-
-
- -
-
-
- - +
+ + Maximum 4000 characters + + + 3982 / 4000 + +
+
+
+
+ +
+
+
+ +
+
+ +
+
+
+
`;