diff --git a/.github/scripts/cleanup_scan.sh b/.github/scripts/cleanup_scan.sh new file mode 100755 index 000000000..24878716f --- /dev/null +++ b/.github/scripts/cleanup_scan.sh @@ -0,0 +1,204 @@ +#!/bin/bash +# Cleanup Scan +# +# Performs a scan of workloads older than 30s, checks for dangling secrets, pvcs, and configmaps +# This script only lists findings and returns a 0/1 (pass/fail) and does not perform a delete. +# For CI/CD safety, only the counts are output, and users are encouraged to run this locally +# in order to see the names and perform the delete themselves. +# +# Dependencies: curl, oc, jq +# Note: the windows variant of jq has differing newline behaviour +# +set -e # failfast +trap 'echo "Error occurred at line $LINENO while executing function $FUNCNAME"' ERR + +# ENV: +# OC_NAMESPACE: namespace to scan +# SKIP_AUTH: set to true to skip auth and use your existing local kubeconfig +# OC_SERVER: OpenShift server URL +# OC_TOKEN: OpenShift token +# ALLOW_EXPR: expression passed into grep extended search to allow certain resources to be skipped + +# THIRTY_DAYS_IN_SECONDS=2592000 - variables in the jq dont play nice so we will just hardcode it + +help_str() { + echo "Usage: SKIP_AUTH=true OC_NAMESPACE= ./cleanup_scan.sh" + echo "" + echo "Ensure you have curl, oc, and jq installed and available on your path, and have performed a oc login." + echo "" + echo "The ALLOW_EXPR regex is passed to grep -E for resource filtering. To read more run: man grep" + echo "" + echo "After running the script, if any results are found you can cat any of the following files:" + echo "cat /tmp/old_workloads_to_delete.txt;" + echo "cat /tmp/secrets_to_delete.txt;" + echo "cat /tmp/pvcs_to_delete.txt;" + echo "cat /tmp/configmaps_to_delete.txt;" + echo "" + echo "Note that these respective files only exist if results are found." +} + +# Configure globals +if [ -z "$ALLOW_EXPR" ]; then + ALLOW_EXPR="default|pipeline|artifact|vault|deployer|logging|builder|keycloak|openshift|bundle|kube|cypress|object-store" +fi +echo "ALLOW_EXPR: $ALLOW_EXPR" + +OC_TEMP_TOKEN="" +if [ -z "$OC_NAMESPACE" ]; then + echo "OC_NAMESPACE is not set. Exiting..." + help_str + exit 1 +fi +if [ "$SKIP_AUTH" != "true" ]; then + if [ -z "$OC_SERVER" ]; then + echo "OC_SERVER is not set. Exiting..." + help_str + exit 1 + fi + if [ -z "$OC_TOKEN" ]; then + echo "OC_TOKEN is not set. Exiting..." + help_str + exit 1 + fi + # Auth flow + OC_TEMP_TOKEN=$(curl -k -X POST $OC_SERVER/api/v1/namespaces/$OC_NAMESPACE/serviceaccounts/pipeline/token --header "Authorization: Bearer $OC_TOKEN" -d '{"spec": {"expirationSeconds": 600}}' -H 'Content-Type: application/json; charset=utf-8' | jq -r '.status.token' ) + oc login --token=$OC_TEMP_TOKEN --server=$OC_SERVER + oc project $OC_NAMESPACE # Safeguard! +fi +OK=0 + +# checks if the resource name appears in the allow expression +# and removes it if found, preventing a false positive +filter_items() { + local items="$1" + local filtered_items=() + # convert items to an array for easy manipulation + local iter_array_items=() + mapfile -t array_items < <(echo "$items") + for item in "${array_items[@]}"; do + if ! echo "$item" | grep -Eq "$ALLOW_EXPR"; then + iter_array_items+=("$item") + fi + done + # organize the filtered items + for item in "${iter_array_items[@]}"; do + if [[ -n "$item" ]]; then + filtered_items+=("$item") + fi + done + duplicates_result=$(printf "%s\n" "${filtered_items[@]}") + unique_result=$(echo "$duplicates_result" | sort | uniq) + echo "$unique_result" +} + +# standard function to output found deletions to a file, and set the OK flag +# note that the message can contain NUM_ITEMS which will be replaced with the count found +found_deletions() { + local file_name=$1 + local err_message=$2 + local ok_message=$3 + local items=$4 + local num_items + num_items=$(echo "$items" | grep -cve '^\s*$' || echo 0) + num_items=${num_items//[^0-9]/} # ensure num_items is an integer + if [ "$num_items" -gt 0 ]; then + echo -e "$items" > "/tmp/$file_name" + echo "${err_message//NUM_ITEMS/$num_items}" + OK=1 + else + echo "$ok_message" + fi +} + +# First, get workloads older than 30 days +echo "Scanning for workloads older than 30 days in the targeted namespace" +echo "..." +old_workloads=$(oc get deploy,service,statefulset -n $OC_NAMESPACE -ojson | jq -r '.items[] | select(.metadata.creationTimestamp | fromdateiso8601 | (. + 2592000) < now) | "\(.kind)/\(.metadata.name)"') +old_workloads=$(filter_items "$old_workloads") +old_workloads_to_delete=$old_workloads +found_deletions \ + "old_workloads_to_delete.txt" \ + "Found NUM_ITEMS workloads older than 30 days in the targeted namespace" \ + "Found no stale workloads in the targeted namespace" \ + "$old_workloads_to_delete" +echo "" +echo "" + +# next get all secrets not used by a pod older than 30 days +# Get all secret names used by pods and write to a file +echo "Scanning for dangling secrets not used by workloads" +echo "..." +oc get pods -n $OC_NAMESPACE -ojson | jq -r '.items[] | "\(.metadata.name):\(.spec.containers[].envFrom[]?.secretRef.name)"' | grep -v null > /tmp/in_use_secrets.txt +oc get pods -n $OC_NAMESPACE -ojson | jq -r '.items[] | "\(.metadata.name):\(.spec.containers[].env[].valueFrom?.secretKeyRef?.name)"' | grep -v null >> /tmp/in_use_secrets.txt +secret_names=$(oc get secret -n $OC_NAMESPACE -ojson | jq -r '.items[] | select(.metadata.creationTimestamp | fromdateiso8601 | (. + 2592000) < now) | .metadata.name') +secrets_to_delete=() +for secret in $secret_names; do + if ! grep -q $secret /tmp/in_use_secrets.txt; then + secrets_to_delete+=("secret/$secret\n") + fi +done +secrets_list=$(echo -e "${secrets_to_delete[@]}") +filtered_secrets=$(filter_items "$secrets_list") +found_deletions \ + "secrets_to_delete.txt" \ + "Found NUM_ITEMS dangling secrets older than 30 days in the targeted namespace" \ + "Found no stale and dangling secrets in the targeted namespace" \ + "${filtered_secrets}" +echo "" +echo "" + +# next get all pvcs not used by a pod +# Get all pvc names used by pods and write to a file +echo "Scanning for dangling pvcs not used by workloads" +echo "..." +oc get pods -n $OC_NAMESPACE -ojson | jq -r '.items[] | "\(.metadata.name):\(.spec.volumes[]?.persistentVolumeClaim.claimName)"' > /tmp/in_use_pvc.txt +in_use_pvcs=$(cat /tmp/in_use_pvc.txt | grep -v "null" | sort | uniq) +echo -e "$in_use_pvcs" > /tmp/in_use_pvc.txt +pvc_list=$(oc get pvc -n $OC_NAMESPACE -ojson | jq -r '.items[] | select(.metadata.creationTimestamp | fromdateiso8601 | (. + 2592000) < now) | .metadata.name') +pvcs_to_delete=() +for pvc in $pvc_list; do + if ! grep -q $pvc /tmp/in_use_pvc.txt; then + pvcs_to_delete+=("pvc/$pvc\n") + fi +done +pvc_list=$(echo -e "${pvcs_to_delete[@]}") +filtered_pvcs=$(filter_items "$pvc_list") +found_deletions \ + "pvcs_to_delete.txt" \ + "Found NUM_ITEMS dangling PVCs older than 30 days in the targeted namespace" \ + "Found no stale and dangling PVCs in the targeted namespace" \ + "${filtered_pvcs}" +echo "" +echo "" + +# next get all configmaps not used by a pod +echo "Scanning for dangling configmaps not used by workloads" +echo "..." +oc get pods -n $OC_NAMESPACE -ojson | jq -r '.items[] | "\(.metadata.name):\(.spec.volumes[]?.configMap.name)"' > /tmp/in_use_configmaps.txt +configmap_names=$(oc get configmap -n $OC_NAMESPACE -ojson | jq -r '.items[] | select(.metadata.creationTimestamp | fromdateiso8601 | (. + 2592000) < now) | .metadata.name') +configmaps_to_delete=() +for configmap in $configmap_names; do + if ! grep -q $configmap /tmp/in_use_configmaps.txt; then + configmaps_to_delete+=("configmap/$configmap\n") + fi +done +configmap_list=$(echo -e "${configmaps_to_delete[@]}") +filtered_configmaps=$(filter_items "$configmap_list") +found_deletions \ + "configmaps_to_delete.txt" \ + "Found NUM_ITEMS dangling configmaps older than 30 days in the targeted namespace" \ + "Found no stale and dangling configmaps in the targeted namespace" \ + "${filtered_configmaps}" +echo "" +echo "" + +if [ $OK -eq 1 ]; then + echo "To delete these found workloads, locally run the following to see them:" + echo "Note: skip flag uses your existing oc authentication" + echo "" + echo "ALLOW_EXPR=\"$ALLOW_EXPR\" SKIP_AUTH=true ./.github/scripts/cleanup_scan.sh;" + echo "cat /tmp/old_workloads_to_delete.txt; cat /tmp/secrets_to_delete.txt; cat /tmp/pvcs_to_delete.txt; cat /tmp/configmaps_to_delete.txt" + +fi + +exit $OK \ No newline at end of file diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index d27440a16..81df41ee8 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -140,6 +140,13 @@ export class AppModule { consumer.apply(HTTPLoggerMiddleware).exclude({ path: "", method: RequestMethod.ALL }).forRoutes("*"); consumer .apply(RequestTokenMiddleware) - .forRoutes("v1/code-table", "v1/case", "v1/configuration", "v1/complaint/search", "v1/complaint/map/search"); + .forRoutes( + "v1/code-table", + "v1/case", + "v1/configuration", + "v1/complaint/search", + "v1/complaint/map/search", + "v1/document/export-complaint", + ); } } diff --git a/backend/src/external_api/case_management.ts b/backend/src/external_api/case_management.ts index ff8dfceff..39f8c2a33 100644 --- a/backend/src/external_api/case_management.ts +++ b/backend/src/external_api/case_management.ts @@ -7,6 +7,136 @@ axios.interceptors.response.use(undefined, (error: AxiosError) => { return Promise.reject(error); }); +export const caseFileQueryFields: string = ` +{ + caseIdentifier + leadIdentifier + assessmentDetails { + actionNotRequired + actionJustificationCode + actionJustificationShortDescription + actionJustificationLongDescription + actionJustificationActiveIndicator + actions { + actionId + actor + date + actionCode + shortDescription + longDescription + activeIndicator + } + } + isReviewRequired + reviewComplete { + actor + date + actionCode + actionId + activeIndicator + } + preventionDetails { + actions { + actionId + actor + date + actionCode + shortDescription + longDescription + activeIndicator + } + } + note { + note + action { + actor + actionCode + date, + actionId, + activeIndicator + } + } + equipment { + id + typeCode + activeIndicator + address + xCoordinate + yCoordinate + createDate + actions { + actionId + actor + actionCode + date + } + wasAnimalCaptured + }, + subject { + id + species + sex + age + categoryLevel + conflictHistory + outcome + tags { + id + ear + identifier + + order + } + drugs { + id + + vial + drug + amountUsed + injectionMethod + reactions + + remainingUse + amountDiscarded + discardMethod + + order + } + actions { + actionId + actor + actionCode + date + } + order + } + decision { + id + schedule + scheduleLongDescription + sector + sectorLongDescription + discharge + dischargeLongDescription + nonCompliance + nonComplianceLongDescription + rationale + inspectionNumber + leadAgency + leadAgencyLongDescription + assignedTo + actionTaken + actionTakenLongDescription + actionTakenDate + } + authorization { + id + type + value + } +} +`; + export const get = (token, params?: {}) => { let config: AxiosRequestConfig = { headers: { diff --git a/backend/src/external_api/cdogs/cdogs.service.ts b/backend/src/external_api/cdogs/cdogs.service.ts index af6d1d157..08d14ef5b 100644 --- a/backend/src/external_api/cdogs/cdogs.service.ts +++ b/backend/src/external_api/cdogs/cdogs.service.ts @@ -123,10 +123,23 @@ export class CdogsService implements ExternalApiService { upload = async (apiToken: string, type: string, templateCode: string) => { const url = `${this.baseUri}/api/v2/template`; - const template = - type === "HWCR" - ? "templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx" - : "templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx"; + let template: string; + + switch (templateCode) { + case "HWCTMPLATE": + template = "templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx"; + break; + case "ERSTMPLATE": + template = "templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx"; + break; + case "CEEBTMPLAT": + template = "templates/complaint/CDOGS-CEEB-COMPLAINT-TEMPLATE-v1.docx"; + break; + default: + this.logger.error(`exception: unable to find template: ${template}`); + break; + } + const path = join(process.cwd(), template); try { @@ -166,7 +179,20 @@ export class CdogsService implements ExternalApiService { //-- render complaint to pdf //-- generate = async (documentName: string, data: any, type: COMPLAINT_TYPE): Promise => { - const templateCode = type === "HWCR" ? CONFIGURATION_CODES.HWCTMPLATE : CONFIGURATION_CODES.ERSTMPLATE; + //-- Determine template to use + let templateCode: string; + switch (type) { + case "HWCR": + templateCode = CONFIGURATION_CODES.HWCTMPLATE; + break; + case "ERS": + if (data.ownedBy === "EPO") { + templateCode = CONFIGURATION_CODES.CEEBTMPLATE; + } else { + templateCode = CONFIGURATION_CODES.ERSTMPLATE; + } + break; + } try { const apiToken = await this.authenticate(); diff --git a/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts b/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts index 4a1f6562e..7dcf9a5c7 100644 --- a/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts +++ b/backend/src/middleware/maps/automapper-entity-to-dto-maps.ts @@ -1189,6 +1189,10 @@ export const mapWildlifeReport = (mapper: Mapper, tz: string = "America/Vancouve (destination) => destination.createdBy, mapFrom((source) => source.create_user_id), ), + forMember( + (destination) => destination.ownedBy, + mapFrom((source) => source.complaint_identifier.owned_by_agency_code.agency_code), + ), forMember( (destination) => destination.reportedOn, mapFrom((source) => { @@ -1487,6 +1491,10 @@ export const mapAllegationReport = (mapper: Mapper, tz: string = "America/Vancou (destination) => destination.createdBy, mapFrom((source) => source.create_user_id), ), + forMember( + (destination) => destination.ownedBy, + mapFrom((source) => source.complaint_identifier.owned_by_agency_code.agency_code), + ), forMember( (destination) => destination.reportedOn, mapFrom((source) => { diff --git a/backend/src/types/configuration-codes.ts b/backend/src/types/configuration-codes.ts index 179017f2e..f63b9244d 100644 --- a/backend/src/types/configuration-codes.ts +++ b/backend/src/types/configuration-codes.ts @@ -1,6 +1,7 @@ export const CONFIGURATION_CODES = { HWCTMPLATE: "HWCTMPLATE", ERSTMPLATE: "ERSTMPLATE", + CEEBTMPLATE: "CEEBTMPLAT", CDTABLEVER: "CDTABLEVER", DFLTPAGNUM: "DFLTPAGNUM", MAXFILESZ: "MAXFILESZ", diff --git a/backend/src/types/models/case-files/case-file.ts b/backend/src/types/models/case-files/case-file.ts index 82bb89fe4..3557ce8bc 100644 --- a/backend/src/types/models/case-files/case-file.ts +++ b/backend/src/types/models/case-files/case-file.ts @@ -4,6 +4,8 @@ import { EquipmentDetailsDto } from "./equipment/equipment-details"; import { Note } from "./supplemental-notes/note"; import { PreventionDetailsDto } from "./prevention-details"; import { FileReviewActionDto } from "./file-review-action"; +import { PermitSiteDto } from "./ceeb/site/permit-site-input"; +import { DecisionDto } from "./ceeb/decision/decision-input"; export interface CaseFileDto { caseIdentifier: UUID; @@ -18,4 +20,6 @@ export interface CaseFileDto { isReviewRequired: boolean; reviewComplete: FileReviewActionDto; note?: Note; + authorization?: PermitSiteDto; + decision?: DecisionDto; } diff --git a/backend/src/types/models/case-files/ceeb/decision/create-decision-input.ts b/backend/src/types/models/case-files/ceeb/decision/create-decision-input.ts index 1fc0bc61b..6501c77b2 100644 --- a/backend/src/types/models/case-files/ceeb/decision/create-decision-input.ts +++ b/backend/src/types/models/case-files/ceeb/decision/create-decision-input.ts @@ -1,6 +1,6 @@ import { BaseCaseFileInput } from "../../base-case-file-input"; -import { DecisionInput } from "./decision-input"; +import { DecisionDto } from "./decision-input"; export interface CreateDecisionInput extends BaseCaseFileInput { - decison: DecisionInput; + decison: DecisionDto; } diff --git a/backend/src/types/models/case-files/ceeb/decision/decision-input.ts b/backend/src/types/models/case-files/ceeb/decision/decision-input.ts index 8d9480201..2c01541e8 100644 --- a/backend/src/types/models/case-files/ceeb/decision/decision-input.ts +++ b/backend/src/types/models/case-files/ceeb/decision/decision-input.ts @@ -1,4 +1,4 @@ -export interface DecisionInput { +export interface DecisionDto { id?: string; schedule: string; sector: string; diff --git a/backend/src/types/models/case-files/ceeb/decision/update-decison-input.ts b/backend/src/types/models/case-files/ceeb/decision/update-decison-input.ts index f6eec2c08..99889b7e1 100644 --- a/backend/src/types/models/case-files/ceeb/decision/update-decison-input.ts +++ b/backend/src/types/models/case-files/ceeb/decision/update-decison-input.ts @@ -1,8 +1,8 @@ import { BaseCaseFileInput } from "../../base-case-file-input"; -import { DecisionInput } from "./decision-input"; +import { DecisionDto } from "./decision-input"; export interface UpdateDecisionInput extends BaseCaseFileInput { agencyCode: string; caseCode: string; - decison: DecisionInput; + decison: DecisionDto; } diff --git a/backend/src/types/models/case-files/ceeb/site/create-authorization-outcome-input.ts b/backend/src/types/models/case-files/ceeb/site/create-authorization-outcome-input.ts index e7c8e3932..c284f2924 100644 --- a/backend/src/types/models/case-files/ceeb/site/create-authorization-outcome-input.ts +++ b/backend/src/types/models/case-files/ceeb/site/create-authorization-outcome-input.ts @@ -1,6 +1,6 @@ import { BaseCaseFileInput } from "../../base-case-file-input"; -import { PermitSiteInput } from "./permit-site-input"; +import { PermitSiteDto } from "./permit-site-input"; export interface CreateAuthorizationOutcomeInput extends BaseCaseFileInput { - input: PermitSiteInput; + input: PermitSiteDto; } diff --git a/backend/src/types/models/case-files/ceeb/site/permit-site-input.ts b/backend/src/types/models/case-files/ceeb/site/permit-site-input.ts index a3d9d9c66..4a81764b7 100644 --- a/backend/src/types/models/case-files/ceeb/site/permit-site-input.ts +++ b/backend/src/types/models/case-files/ceeb/site/permit-site-input.ts @@ -1,4 +1,4 @@ -export interface PermitSiteInput { +export interface PermitSiteDto { id?: string; type: "permit" | "site"; value: string; diff --git a/backend/src/types/models/case-files/ceeb/site/update-authorization-outcome-input.ts b/backend/src/types/models/case-files/ceeb/site/update-authorization-outcome-input.ts index 18ea496fd..734f4a06c 100644 --- a/backend/src/types/models/case-files/ceeb/site/update-authorization-outcome-input.ts +++ b/backend/src/types/models/case-files/ceeb/site/update-authorization-outcome-input.ts @@ -1,6 +1,6 @@ import { BaseCaseFileInput } from "../../base-case-file-input"; -import { PermitSiteInput } from "./permit-site-input"; +import { PermitSiteDto } from "./permit-site-input"; export interface UpdateAuthorizationOutcomeInput extends BaseCaseFileInput { - input: PermitSiteInput; + input: PermitSiteDto; } diff --git a/backend/src/types/models/reports/complaints/complaint-report-data.ts b/backend/src/types/models/reports/complaints/complaint-report-data.ts index 23d370005..76f368e4f 100644 --- a/backend/src/types/models/reports/complaints/complaint-report-data.ts +++ b/backend/src/types/models/reports/complaints/complaint-report-data.ts @@ -9,6 +9,7 @@ export interface ComplaintReportData { generatedOn: string; updatedOn: Date | string; createdBy: string; + ownedBy: string; officerAssigned: string; status: string; incidentDateTime: Date | string; diff --git a/backend/src/v1/case_file/case_file.module.ts b/backend/src/v1/case_file/case_file.module.ts index 843f4961c..6e09f47e5 100644 --- a/backend/src/v1/case_file/case_file.module.ts +++ b/backend/src/v1/case_file/case_file.module.ts @@ -18,5 +18,6 @@ import { LinkedComplaintXref } from "../linked_complaint_xref/entities/linked_co ], controllers: [CaseFileController], providers: [CaseFileService], + exports: [CaseFileService], }) export class CaseFileModule {} diff --git a/backend/src/v1/case_file/case_file.service.spec.ts b/backend/src/v1/case_file/case_file.service.spec.ts index b489861c4..8b30052bc 100644 --- a/backend/src/v1/case_file/case_file.service.spec.ts +++ b/backend/src/v1/case_file/case_file.service.spec.ts @@ -64,6 +64,13 @@ import { TeamCode } from "../team_code/entities/team_code.entity"; import { CompMthdRecvCdAgcyCdXref } from "../comp_mthd_recv_cd_agcy_cd_xref/entities/comp_mthd_recv_cd_agcy_cd_xref"; import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; import { LinkedComplaintXref } from "../linked_complaint_xref/entities/linked_complaint_xref.entity"; +import { OfficerService } from "../officer/officer.service"; +import { PersonService } from "../person/person.service"; +import { OfficeService } from "../office/office.service"; +import { CssService } from "../../external_api/css/css.service"; +import { ConfigurationService } from "../configuration/configuration.service"; +import { Configuration } from "../configuration/entities/configuration.entity"; +import { Person } from "../person/entities/person.entity"; describe("Testing: Case File Service", () => { let service: CaseFileService; @@ -192,10 +199,23 @@ describe("Testing: Case File Service", () => { provide: getRepositoryToken(LinkedComplaintXref), useValue: {}, }, + { + provide: getRepositoryToken(Configuration), + useValue: {}, + }, + { + provide: getRepositoryToken(Person), + useValue: {}, + }, ComplaintUpdatesService, CaseFileService, ComplaintService, CodeTableService, + OfficerService, + OfficeService, + CssService, + ConfigurationService, + PersonService, PersonComplaintXrefService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, diff --git a/backend/src/v1/case_file/case_file.service.ts b/backend/src/v1/case_file/case_file.service.ts index 892786d5e..a33c0c4d6 100644 --- a/backend/src/v1/case_file/case_file.service.ts +++ b/backend/src/v1/case_file/case_file.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, Logger, Scope } from "@nestjs/common"; import { InjectMapper } from "@automapper/nestjs"; import { Mapper } from "@automapper/core"; -import { get, post } from "../../external_api/case_management"; +import { caseFileQueryFields, get, post } from "../../external_api/case_management"; import { CaseFileDto } from "src/types/models/case-files/case-file"; import { REQUEST } from "@nestjs/core"; import { AxiosResponse, AxiosError } from "axios"; @@ -32,130 +32,6 @@ export class CaseFileService { private readonly logger = new Logger(CaseFileService.name); private mapper: Mapper; - private caseFileQueryFields: string = ` - { - caseIdentifier - leadIdentifier - assessmentDetails { - actionNotRequired - actionJustificationCode - actionJustificationShortDescription - actionJustificationLongDescription - actionJustificationActiveIndicator - actions { - actionId - actor - date - actionCode - shortDescription - longDescription - activeIndicator - } - } - isReviewRequired - reviewComplete { - actor - date - actionCode - actionId - activeIndicator - } - preventionDetails { - actions { - actionId - actor - date - actionCode - shortDescription - longDescription - activeIndicator - } - } - note { - note - action { - actor - actionCode - date, - actionId, - activeIndicator - } - } - equipment { - id - typeCode - activeIndicator - address - xCoordinate - yCoordinate - createDate - actions { - actionId - actor - actionCode - date - } - wasAnimalCaptured - }, - subject { - id - species - sex - age - categoryLevel - conflictHistory - outcome - tags { - id - ear - identifier - - order - } - drugs { - id - - vial - drug - amountUsed - injectionMethod - reactions - - remainingUse - amountDiscarded - discardMethod - - order - } - actions { - actionId - actor - actionCode - date - } - order - } - decision { - id - schedule - sector - discharge - nonCompliance - rationale - inspectionNumber - leadAgency - assignedTo - actionTaken - actionTakenDate - } - authorization { - id - type - value - } - } - `; - constructor( @Inject(REQUEST) private request: Request, @InjectMapper() mapper, @@ -169,7 +45,7 @@ export class CaseFileService { find = async (complaint_id: string, token: string): Promise => { const { data, errors } = await get(token, { query: `{getCaseFileByLeadId (leadIdentifier: "${complaint_id}") - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, }); @@ -291,7 +167,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation CreateAssessment($createAssessmentInput: CreateAssessmentInput!) { createAssessment(createAssessmentInput: $createAssessmentInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -310,7 +186,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation CreateAssessment($createAssessmentInput: CreateAssessmentInput!) { createAssessment(createAssessmentInput: $createAssessmentInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -324,7 +200,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation UpdateAssessment($updateAssessmentInput: UpdateAssessmentInput!) { updateAssessment(updateAssessmentInput: $updateAssessmentInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -336,7 +212,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation CreateReview($reviewInput: ReviewInput!) { createReview(reviewInput: $reviewInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -359,7 +235,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation UpdateReview($reviewInput: ReviewInput!) { updateReview(reviewInput: $reviewInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -388,7 +264,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation CreatePrevention($createPreventionInput: CreatePreventionInput!) { createPrevention(createPreventionInput: $createPreventionInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -400,7 +276,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation UpdatePrevention($updatePreventionInput: UpdatePreventionInput!) { updatePrevention(updatePreventionInput: $updatePreventionInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); @@ -427,7 +303,7 @@ export class CaseFileService { const mutationQuery = { query: `mutation CreateEquipment($createEquipmentInput: CreateEquipmentInput!) { createEquipment(createEquipmentInput: $createEquipmentInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }; @@ -444,7 +320,7 @@ export class CaseFileService { const result = await post(token, { query: `mutation UpdateEquipment($updateEquipmentInput: UpdateEquipmentInput!) { updateEquipment(updateEquipmentInput: $updateEquipmentInput) - ${this.caseFileQueryFields} + ${caseFileQueryFields} }`, variables: model, }); diff --git a/backend/src/v1/complaint/complaint.module.ts b/backend/src/v1/complaint/complaint.module.ts index 14ca39afc..daa4fc6fc 100644 --- a/backend/src/v1/complaint/complaint.module.ts +++ b/backend/src/v1/complaint/complaint.module.ts @@ -33,6 +33,7 @@ import { CompMthdRecvCdAgcyCdXref } from "../comp_mthd_recv_cd_agcy_cd_xref/enti import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; import { CompMthdRecvCdAgcyCdXrefModule } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.module"; import { LinkedComplaintXrefModule } from "../linked_complaint_xref/linked_complaint_xref.module"; +import { OfficerModule } from "../officer/officer.module"; @Module({ imports: [ @@ -67,6 +68,7 @@ import { LinkedComplaintXrefModule } from "../linked_complaint_xref/linked_compl StagingComplaintModule, CompMthdRecvCdAgcyCdXrefModule, LinkedComplaintXrefModule, + OfficerModule, ], controllers: [ComplaintController], providers: [ComplaintService, CompMthdRecvCdAgcyCdXrefService], diff --git a/backend/src/v1/complaint/complaint.service.spec.ts b/backend/src/v1/complaint/complaint.service.spec.ts index a49e67c54..3b8f00787 100644 --- a/backend/src/v1/complaint/complaint.service.spec.ts +++ b/backend/src/v1/complaint/complaint.service.spec.ts @@ -70,6 +70,13 @@ import { StagingComplaint } from "../staging_complaint/entities/staging_complain import { TeamCode } from "../team_code/entities/team_code.entity"; import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; import { CompMthdRecvCdAgcyCdXref } from "../comp_mthd_recv_cd_agcy_cd_xref/entities/comp_mthd_recv_cd_agcy_cd_xref"; +import { OfficerService } from "../officer/officer.service"; +import { PersonService } from "../person/person.service"; +import { OfficeService } from "../office/office.service"; +import { CssService } from "../../external_api/css/css.service"; +import { ConfigurationService } from "../configuration/configuration.service"; +import { Person } from "../person/entities/person.entity"; +import { Configuration } from "../configuration/entities/configuration.entity"; describe("Testing: Complaint Service", () => { let service: ComplaintService; @@ -93,9 +100,22 @@ describe("Testing: Complaint Service", () => { provide: getRepositoryToken(ActionTaken), useValue: {}, }, + { + provide: getRepositoryToken(Configuration), + useValue: {}, + }, + { + provide: getRepositoryToken(Person), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, PersonComplaintXrefService, + OfficerService, + OfficeService, + CssService, + ConfigurationService, + PersonService, AttractantHwcrXrefService, CodeTableService, CompMthdRecvCdAgcyCdXrefService, @@ -360,9 +380,22 @@ describe("Testing: Complaint Service", () => { provide: getRepositoryToken(ActionTaken), useValue: {}, }, + { + provide: getRepositoryToken(Configuration), + useValue: {}, + }, + { + provide: getRepositoryToken(Person), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, PersonComplaintXrefService, + OfficerService, + OfficeService, + CssService, + ConfigurationService, + PersonService, AttractantHwcrXrefService, CodeTableService, CompMthdRecvCdAgcyCdXrefService, diff --git a/backend/src/v1/complaint/complaint.service.ts b/backend/src/v1/complaint/complaint.service.ts index 64b139cf3..b9b75797e 100644 --- a/backend/src/v1/complaint/complaint.service.ts +++ b/backend/src/v1/complaint/complaint.service.ts @@ -4,7 +4,7 @@ import { InjectRepository } from "@nestjs/typeorm"; import { Brackets, DataSource, QueryRunner, Repository, SelectQueryBuilder } from "typeorm"; import { InjectMapper } from "@automapper/nestjs"; import { Mapper } from "@automapper/core"; -import { get } from "../../external_api/case_management"; +import { caseFileQueryFields, get } from "../../external_api/case_management"; import { applyAllegationComplaintMap, @@ -66,6 +66,7 @@ import { WildlifeReportData } from "src/types/models/reports/complaints/wildlife import { AllegationReportData } from "src/types/models/reports/complaints/allegation-report-data"; import { RelatedDataDto } from "src/types/models/complaints/related-data"; import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; +import { OfficerService } from "../officer/officer.service"; type complaintAlias = HwcrComplaint | AllegationComplaint | GirComplaint; @Injectable({ scope: Scope.REQUEST }) @@ -91,13 +92,15 @@ export class ComplaintService { private _cosOrganizationUnitRepository: Repository; constructor( - @Inject(REQUEST) private request: Request, + @Inject(REQUEST) + private readonly request: Request, @InjectMapper() mapper, private readonly _codeTableService: CodeTableService, private readonly _compliantUpdatesService: ComplaintUpdatesService, private readonly _personService: PersonComplaintXrefService, private readonly _attractantService: AttractantHwcrXrefService, private readonly _compMthdRecvCdAgcyCdXrefService: CompMthdRecvCdAgcyCdXrefService, + private readonly _officerService: OfficerService, private dataSource: DataSource, ) { this.mapper = mapper; @@ -128,12 +131,15 @@ export class ComplaintService { .where("officer.user_id = :idir", { idir }); const result = await builder.getOne(); - //-- pull the user's agency from the query results and return the agency code - const { - office_guid: { agency_code }, - } = result; - return agency_code; + if (result.office_guid?.agency_code) { + const { + office_guid: { agency_code }, + } = result; + return agency_code; + } else { + return null; + } }; private _getSortTable = (column: string): string => { @@ -986,9 +992,10 @@ export class ComplaintService { try { let results: MapSearchResults = { complaints: [], unmappedComplaints: 0 }; - //-- get the users assigned agency - const agency = await this._getAgencyByUser(); - + //-- assign the users agency + // _getAgencyByUser traces agency through assigned office of the officer, which CEEB users do not have + // so the hasCEEBRole is used to assign agency for them. + const agency = hasCEEBRole ? "EPO" : (await this._getAgencyByUser()).agency_code; //-- search for complaints let complaintBuilder = this._generateQueryBuilder(complaintType); @@ -1005,7 +1012,7 @@ export class ComplaintService { //-- only return complaints for the agency the user is associated with if (agency) { complaintBuilder.andWhere("complaint.owned_by_agency_code.agency_code = :agency", { - agency: agency.agency_code, + agency: agency, }); } @@ -1035,7 +1042,7 @@ export class ComplaintService { //-- only return complaints for the agency the user is associated with if (agency) { unMappedBuilder.andWhere("complaint.owned_by_agency_code.agency_code = :agency", { - agency: agency.agency_code, + agency: agency, }); } @@ -1508,7 +1515,7 @@ export class ComplaintService { return results; }; - getReportData = async (id: string, complaintType: COMPLAINT_TYPE, tz: string) => { + getReportData = async (id: string, complaintType: COMPLAINT_TYPE, tz: string, token: string) => { let data; mapWildlifeReport(this.mapper, tz); mapAllegationReport(this.mapper, tz); @@ -1571,6 +1578,42 @@ export class ComplaintService { } }; + const _getCaseData = async (id: string, token: string) => { + //-- Get the Outcome Data, this is done via a GQL call to prevent + //-- a circular dependency between the complaint and case_file modules + const { data, errors } = await get(token, { + query: `{getCaseFileByLeadId (leadIdentifier: "${id}") + ${caseFileQueryFields} + }`, + }); + if (errors) { + this.logger.error("GraphQL errors:", errors); + throw new Error("GraphQL errors occurred"); + } + + //-- Clean up the data to make it easier for formatting + let outcomeData = data; + //-- Add UA to unpermitted sites + if ( + outcomeData.getCaseFileByLeadId.authorization && + outcomeData.getCaseFileByLeadId.authorization.type !== "permit" + ) { + outcomeData.getCaseFileByLeadId.authorization.value = + "UA" + outcomeData.getCaseFileByLeadId.authorization.value; + } + + //-- Convert Officer Guids to Names + if (outcomeData.getCaseFileByLeadId.note) { + const { first_name, last_name } = ( + await this._officerService.findByAuthUserGuid(outcomeData.getCaseFileByLeadId.note.action.actor) + ).person_guid; + + outcomeData.getCaseFileByLeadId.note.action.actor = last_name + ", " + first_name; + } + + return outcomeData.getCaseFileByLeadId; + }; + try { if (complaintType) { builder = this._generateQueryBuilder(complaintType); @@ -1625,19 +1668,28 @@ export class ComplaintService { "AllegationComplaint", "AllegationReportData", ); - - //-- this is a bit of a hack to hide and show the privacy requested row - if (data.privacyRequested) { - data = { ...data, privacy: [{ value: data.privacyRequested }] }; - } - break; } } + //-- get case data + data.outcome = await _getCaseData(id, token); + //-- get any updates a complaint may have data.updates = await _getUpdates(id); + //-- this is a workaround to hide empty rows in the carbone templates + //-- It could possibly be removed if the CDOGS version of Carbone is updated + if (data.privacyRequested) { + data = { ...data, privacy: [{ value: data.privacyRequested }] }; + } + if (data.outcome.decision?.leadAgencyLongDescription) { + data = { ...data, agency: [{ value: data.outcome.decision.leadAgencyLongDescription }] }; + } + if (data.outcome.decision?.inspectionNumber) { + data = { ...data, inspection: [{ value: data.outcome.decision.inspectionNumber }] }; + } + //-- problems in the automapper mean dates need to be handled //-- seperatly const current = new Date(); @@ -1648,6 +1700,14 @@ export class ComplaintService { data.reportedOn = _applyTimezone(data.reportedOn, tz, "datetime"); data.updatedOn = _applyTimezone(data.updatedOn, tz, "datetime"); + if (data.outcome.note) { + data.outcome.note.action.date = _applyTimezone(data.outcome.note.action.date, tz, "date"); + } + + if (data.outcome.decision) { + data.outcome.decision.actionTakenDate = _applyTimezone(data.outcome.decision.actionTakenDate, tz, "date"); + } + //-- incidentDateTime may not be set, if there's no date //-- don't try and apply the incident date if (data.incidentDateTime) { diff --git a/backend/src/v1/document/document.controller.spec.ts b/backend/src/v1/document/document.controller.spec.ts index b907efb57..1b132450e 100644 --- a/backend/src/v1/document/document.controller.spec.ts +++ b/backend/src/v1/document/document.controller.spec.ts @@ -67,6 +67,11 @@ import { StagingComplaint } from "../staging_complaint/entities/staging_complain import { TeamCode } from "../team_code/entities/team_code.entity"; import { CompMthdRecvCdAgcyCdXref } from "../comp_mthd_recv_cd_agcy_cd_xref/entities/comp_mthd_recv_cd_agcy_cd_xref"; import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; +import { OfficerService } from "../officer/officer.service"; +import { PersonService } from "../person/person.service"; +import { OfficeService } from "../office/office.service"; +import { CssService } from "../../external_api/css/css.service"; +import { Person } from "../person/entities/person.entity"; describe("DocumentController", () => { let controller: DocumentController; @@ -191,10 +196,18 @@ describe("DocumentController", () => { provide: getRepositoryToken(CompMthdRecvCdAgcyCdXref), useFactory: MockCompMthdRecvCdAgcyCdXrefRepository, }, + { + provide: getRepositoryToken(Person), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, CodeTableService, PersonComplaintXrefService, + OfficerService, + OfficeService, + CssService, + PersonService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, { diff --git a/backend/src/v1/document/document.controller.ts b/backend/src/v1/document/document.controller.ts index 2fab8532c..4311e3d23 100644 --- a/backend/src/v1/document/document.controller.ts +++ b/backend/src/v1/document/document.controller.ts @@ -29,7 +29,7 @@ export class DocumentController { ): Promise { try { const fileName = `Complaint-${id}-${type}-${format(new Date(), "yyyy-MM-dd")}.pdf`; - const response = await this.service.exportComplaint(id, type, fileName, tz); + const response = await this.service.exportComplaint(id, type, fileName, tz, token); if (!response || !response.data) { throw Error(`exception: unable to export document for complaint: ${id}`); diff --git a/backend/src/v1/document/document.service.spec.ts b/backend/src/v1/document/document.service.spec.ts index 359e45aa4..11ef41b49 100644 --- a/backend/src/v1/document/document.service.spec.ts +++ b/backend/src/v1/document/document.service.spec.ts @@ -66,6 +66,11 @@ import { StagingComplaint } from "../staging_complaint/entities/staging_complain import { TeamCode } from "../team_code/entities/team_code.entity"; import { CompMthdRecvCdAgcyCdXrefService } from "../comp_mthd_recv_cd_agcy_cd_xref/comp_mthd_recv_cd_agcy_cd_xref.service"; import { CompMthdRecvCdAgcyCdXref } from "../comp_mthd_recv_cd_agcy_cd_xref/entities/comp_mthd_recv_cd_agcy_cd_xref"; +import { OfficerService } from "../officer/officer.service"; +import { PersonService } from "../person/person.service"; +import { OfficeService } from "../office/office.service"; +import { CssService } from "../../external_api/css/css.service"; +import { Person } from "../person/entities/person.entity"; describe("DocumentService", () => { let service: DocumentService; @@ -190,10 +195,18 @@ describe("DocumentService", () => { provide: getRepositoryToken(CompMthdRecvCdAgcyCdXref), useFactory: MockCompMthdRecvCdAgcyCdXrefRepository, }, + { + provide: getRepositoryToken(Person), + useValue: {}, + }, ComplaintUpdatesService, ComplaintService, CodeTableService, PersonComplaintXrefService, + OfficerService, + OfficeService, + CssService, + PersonService, AttractantHwcrXrefService, CompMthdRecvCdAgcyCdXrefService, { diff --git a/backend/src/v1/document/document.service.ts b/backend/src/v1/document/document.service.ts index e3939d950..6a2f35b50 100644 --- a/backend/src/v1/document/document.service.ts +++ b/backend/src/v1/document/document.service.ts @@ -17,11 +17,11 @@ export class DocumentService { //-- using the cdogs api generate a new document from the specified //-- complaint-id and complaint type //-- - exportComplaint = async (id: string, type: COMPLAINT_TYPE, name: string, tz: string) => { + exportComplaint = async (id: string, type: COMPLAINT_TYPE, name: string, tz: string, token: string) => { try { //-- get the complaint from the system, but do not include anything other //-- than the base complaint. no maps, no attachments, no outcome data - const data = await this.ceds.getReportData(id, type, tz); + const data = await this.ceds.getReportData(id, type, tz, token); //-- return await this.cdogs.generate(name, data, type); diff --git a/backend/src/v1/officer/officer.module.ts b/backend/src/v1/officer/officer.module.ts index 0e2ce3d35..8b4561be9 100644 --- a/backend/src/v1/officer/officer.module.ts +++ b/backend/src/v1/officer/officer.module.ts @@ -18,5 +18,6 @@ import { CssModule } from "src/external_api/css/css.module"; ], controllers: [OfficerController], providers: [OfficerService, PersonService, OfficeService], + exports: [OfficerService], }) export class OfficerModule {} diff --git a/backend/templates/complaint/CDOGS-CEEB-COMPLAINT-TEMPLATE-v1.docx b/backend/templates/complaint/CDOGS-CEEB-COMPLAINT-TEMPLATE-v1.docx new file mode 100644 index 000000000..7be974984 Binary files /dev/null and b/backend/templates/complaint/CDOGS-CEEB-COMPLAINT-TEMPLATE-v1.docx differ diff --git a/backend/templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx b/backend/templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx index 70030ede8..6e08cbc30 100644 Binary files a/backend/templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx and b/backend/templates/complaint/CDOGS-ERS-COMPLAINT-TEMPLATE-v1.docx differ diff --git a/backend/templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx b/backend/templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx index 7180de5de..f23ed6510 100644 Binary files a/backend/templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx and b/backend/templates/complaint/CDOGS-HWCR-COMPLAINT-TEMPLATE-v1.docx differ diff --git a/migrations/migrations/R__Create-Test-Data.sql b/migrations/migrations/R__Create-Test-Data.sql index 06f7dfd07..f1b4065d2 100644 --- a/migrations/migrations/R__Create-Test-Data.sql +++ b/migrations/migrations/R__Create-Test-Data.sql @@ -9795,6 +9795,32 @@ SELECT now() ON CONFLICT DO NOTHING; +------------------------ +-- New Template: CEEB +------------------------ +INSERT INTO + configuration ( + configuration_code, + configuration_value, + long_description, + active_ind, + create_user_id, + create_utc_timestamp, + update_user_id, + update_utc_timestamp + ) +VALUES + ( + 'CEEBTMPLAT', + '', + 'CDOGS Hash for CEEB Template', + true, + CURRENT_USER, + CURRENT_TIMESTAMP, + CURRENT_USER, + CURRENT_TIMESTAMP + ) ON CONFLICT DO NOTHING; + -------------------------- -- New Changes above this line diff --git a/migrations/migrations/R__reset-templates.sql b/migrations/migrations/R__reset-templates.sql index d0aa25ead..e70d32d46 100644 --- a/migrations/migrations/R__reset-templates.sql +++ b/migrations/migrations/R__reset-templates.sql @@ -11,4 +11,4 @@ UPDATE "configuration" SET configuration_value = '' WHERE - configuration_code IN ('ERSTMPLATE', 'HWCTMPLATE'); \ No newline at end of file + configuration_code IN ('ERSTMPLATE', 'HWCTMPLATE', 'CEEBTMPLATE'); \ No newline at end of file