diff --git a/.envrc b/.envrc index c5e72d09ad2..32fd448864a 100644 --- a/.envrc +++ b/.envrc @@ -415,7 +415,7 @@ if [ ! -r .nix-disable ] && has nix-env; then # add the NIX_PROFILE bin path so that everything we just installed # is available on the path - PATH_add ${NIX_PROFILE}/bin + PATH_add "${NIX_PROFILE}"/bin # Add the node binaries to our path PATH_add ./node_modules/.bin # nix is immutable, so we need to specify a path for local changes, e.g. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dcf1897da35..f83d084326b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ variables: #Docker config DOCKER_AUTH_CONFIG: "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD\"}}}" + #hard code sha as newer version of debian is needed for pre-test DOCKER_APP_IMAGE: milmove01/transcom-docker:milmove-app DOCKER_BASE_IMAGE: milmove01/transcom-docker:base DOCKERHUB_USERNAME: DOCKERHUB_USERNAME @@ -24,7 +25,7 @@ variables: #CIRCLE_TOKEN: "$GITLAB_API_TOKEN" # GitLab API token for querying pipelines CIRCLE_BUILD_NUM: "$CI_PIPELINE_ID" - GOPATH: "$CI_PROJECT_DIR/go" + GOPATH: "/home/transcom/go" #Go path on the app image GOLANGCI_LINT_CONCURRENCY: "4" GOLANGCI_LINT_VERBOSE: "-v" @@ -63,7 +64,7 @@ stages: #set safe directory and path .setup_milmove_env: &setup_milmove_env - git config --global --add safe.directory /builds/milmove/mymove - - export PATH=${PATH}:${GOPATH}/bin:~/transcom/mymove/builds/milmove/mymove:/builds/milmove/mymove/scripts + - export PATH=${PATH}:${GOPATH}/bin:~/transcom/mymove/builds/milmove/mymove:/builds/milmove/mymove/scripts:/builds/milmove/mymove/bin - export REACT_APP_ERROR_LOGGING=otel .announce_failure: &announce_failure @@ -151,7 +152,7 @@ stages: #build off prd variables - export ECR_REPOSITORY_URI=${PRD_ACCOUNT_ID}.dkr.ecr.${PRD_REGION}.amazonaws.com - export APP_DOCKER_FILE=Dockerfile - - export TASK_DOCKER_FILE=Dockerfile + - export TASK_DOCKER_FILE=Dockerfile.tasks #TODO: update exp to prod - export APP_ENVIRONMENT=prd @@ -254,6 +255,7 @@ stages: export OKTA_API_KEY=notrealapikey8675309 export OKTA_OFFICE_GROUP_ID=notrealgroupId export OKTA_CUSTOMER_GROUP_ID=notrealcustomergroupId + export IWS_RBS_HOST=pkict.dmdc.osd.mil .setup_env_intergration_mtls: &setup_env_intergration_mtls - | @@ -389,6 +391,89 @@ pre_deps_golang: # - $GOPATH/pkg/mod # - /builds/milmove/mymove/bin # Ensure this path is correct and writable. # Optionally, you can define an after_script for cleanup or notifications. +golang_lint: + stage: pre_checks + interruptible: true + tags: + - $DOCKER_RUNNER_TAG + image: golangci/golangci-lint:latest # Refer to https://hub.docker.com/r/golangci/golangci-lint + script: + - golangci-lint run --print-issued-lines=false --timeout=25m --out-format code-climate:gl-code-quality-report.json,line-number + artifacts: + reports: + codequality: gl-code-quality-report.json + paths: + - gl-code-quality-report.json + when: always + allow_failure: true + +# WIP but failing and will need to get back to see if this is a viable option for go test coverage +# golang_coverage: +# stage: pre_checks +# interruptible: true +# tags: +# - $DOCKER_RUNNER_TAG +# image: $DOCKER_APP_IMAGE +# services: +# - name: docker:dind +# alias: docker +# - name: $postgres +# - name: $redis +# before_script: +# - *setup_milmove_env +# variables: +# KUBERNETES_CPU_REQUEST: "4" +# KUBERNETES_MEMORY_REQUEST: "8Gi" +# KUBERNETES_MEMORY_LIMIT: "8Gi" +# DOCKER_HOST: "tcp://docker-backend.gitlab-runner.svc.cluster.local:2375" +# DOCKER_TLS_CERTDIR: "" +# APPLICATION: app +# # 8 since this runs on xlarge with 8 CPUs +# GOTEST_PARALLEL: 8 +# DB_PASSWORD: mysecretpassword +# DB_USER_LOW_PRIV: crud +# DB_PASSWORD_LOW_PRIV: mysecretpassword +# DB_USER: postgres +# DB_HOST: localhost +# DB_PORT_TEST: 5432 +# DB_PORT: 5432 +# DB_NAME: test_db +# DB_NAME_TEST: test_db +# DTOD_USE_MOCK: 'true' +# MIGRATION_MANIFEST: '/builds/milmove/mymove/migrations/app/migrations_manifest.txt' +# MIGRATION_PATH: 'file:///builds/milmove/mymove/migrations/app/schema;file:///builds/milmove/mymove/migrations/app/secure' +# EIA_KEY: db2522a43820268a41a802a16ae9fd26 # dummy key generated with openssl rand -hex 16 +# ENV: test +# ENVIRONMENT: test +# SERVER_REPORT: 1 +# COVERAGE: 1 +# SERVE_API_INTERNAL: 'true' +# OKTA_CUSTOMER_CLIENT_ID: 1q2w3e4r5t6y7u8i9o +# OKTA_ADMIN_CLIENT_ID: AQ1SW2DE3FR4G5 +# OKTA_OFFICE_CLIENT_ID: 9f9f9s8s90gig9 +# OKTA_API_KEY: notrealapikey8675309 +# OKTA_OFFICE_GROUP_ID: notrealgroupId +# OKTA_CUSTOMER_GROUP_ID: notrealcustomergroupId +# POSTGRES_DB: test_db #for postgres container +# POSTGRES_USER: postgres +# POSTGRES_PASSWORD: mysecretpassword +# POSTGRES_HOST_AUTH_METHOD: trust +# DPS_AUTH_SECRET_KEY: placeholder +# CSRF_AUTH_KEY: d096fd8529eefaa46497849d11d2ff2e979ddfaed1aff058524ada9bceadd67c +# IWS_RBS_ENABLED: 0 +# IWS_RBS_HOST: "pkict.dmdc.osd.mil" +# script: +# - go test ./... -coverprofile=coverage.txt -covermode count +# - go get github.com/boumenot/gocover-cobertura +# - go run github.com/boumenot/gocover-cobertura < coverage.txt > coverage.xml +# allow_failure: true +# after_script: +# - *announce_failure +# artifacts: +# reports: +# coverage_report: +# coverage_format: cobertura +# path: /builds/milmove/mymove/coverage.xml pre_deps_yarn: stage: pre_checks @@ -639,36 +724,36 @@ pre_test: [ -d ~/transcom/mymove/spectral ] && cp -r ~/transcom/mymove/spectral /tmp/spectral_baseline || echo "Skipping saving baseline" - rm -rf ~/transcom/mymove/spectral - *install_yarn + # this is so we can avoid go mod downloading and resulting in an error on a false positive + - ./scripts/pre-commit-go-mod || exit 0 - echo "Run pre-commit tests without golangci-lint, eslint, or prettier" - - SKIP=golangci-lint,eslint,prettier,ato-go-linter,gomod,appcontext-linter pre-commit run --all-files - - | - echo "Run pre-commit tests with ato-go-linter only" - pre-commit run -v --all-files ato-go-linter - - | - echo "Run pre-commit tests with gomod only" - pre-commit run -v --all-files gomod,appcontext-linter - - | - echo "Run pre-commit tests with appcontext-linter only" - pre-commit run -v --all-files appcontext-linter + - SKIP=golangci-lint,eslint,prettier pre-commit run --all-files - echo "Run pre-commit tests with golangci-lint only" - | - echo 'export GOLANGCI_LINT_CONCURRENCY=4' >> $BASH_ENV - echo 'export GOLANGCI_LINT_VERBOSE=-v' >> $BASH_ENV - source $BASH_ENV + export GOLANGCI_LINT_CONCURRENCY=4 + export GOLANGCI_LINT_VERBOSE=-v mkdir -p tmp/test-results/pretest - pre-commit run -v --all-files golangci-lint | tee tmp/test-results/pretest/golangci-lint.out - - echo "Run prettier, eslint, danger checks" + # can this be removed in favor of golang_lint? + - pre-commit run -v --all-files golangci-lint | tee tmp/test-results/pretest/golangci-lint.out + - echo "Run prettier, eslint, danger checks" - yarn prettier-ci - yarn lint - yarn danger ci --failOnErrors - - echo "Run spectral linter on all files" + - echo "Run spectral linter on all files" - ./scripts/ensure-spectral-lint /tmp/spectral_baseline spectral - - ./scripts/pre-commit-go-mod || exit 0 allow_failure: true after_script: - *announce_failure - rules: - - *check_server_ignore_branch + artifacts: + reports: + codequality: tmp/test-results/pretest/golangci-lint.out + paths: + - tmp/test-results/pretest/golangci-lint.out #remove if golang_lint works + - tmp/spectral_baseline/*.json #what do we need to store for review? + - spectral/*.json #what do we need to store for review? + when: always + # rules: + # - *check_server_ignore_branch server_test: stage: test @@ -698,7 +783,7 @@ server_test: DB_PASSWORD_LOW_PRIV: mysecretpassword DB_USER: postgres DB_HOST: localhost - DB_PORT_TEST: 5433 + DB_PORT_TEST: 5432 DB_PORT: 5432 DB_NAME: test_db DB_NAME_TEST: test_db @@ -717,18 +802,22 @@ server_test: OKTA_API_KEY: notrealapikey8675309 OKTA_OFFICE_GROUP_ID: notrealgroupId OKTA_CUSTOMER_GROUP_ID: notrealcustomergroupId + POSTGRES_DB: test_db #for postgres container + POSTGRES_USER: postgres + POSTGRES_PASSWORD: mysecretpassword + POSTGRES_HOST_AUTH_METHOD: trust + DPS_AUTH_SECRET_KEY: placeholder + CSRF_AUTH_KEY: d096fd8529eefaa46497849d11d2ff2e979ddfaed1aff058524ada9bceadd67c + IWS_RBS_ENABLED: 0 + IWS_RBS_HOST: "pkict.dmdc.osd.mil" script: - - psql --version - for i in $(seq 1 5); do go mod download && break || s=$? && sleep 5; done; (exit $s) - scripts/check-generated-code go.sum - make bin/swagger - - echo "server test -- TODO Add steps need to potentially pass job id to file and persist" + - echo "server test -- build gotestsum and run scripts for report" - make -j 2 bin/milmove bin/gotestsum - make server_test for app - # - go install gotest.tools/gotestsum@latest - # - go mod tidy - #- bin/gotestsum --junitfile server_test_report.xml --format server_test - allow_failure: true + allow_failure: true #leaving true until 5 tests failing tests are working artifacts: paths: - /builds/milmove/mymove/bin/gotestsum @@ -738,8 +827,9 @@ server_test: junit: /builds/milmove/mymove/tmp/test-results/gotest/app/go-test-report.xml after_script: - *announce_failure - rules: - - *check_server_ignore_branch + # we want to make this run on every branch bc webhooks don't exist currently + # rules: + # - *check_server_ignore_branch server_test_coverage: stage: test @@ -806,8 +896,9 @@ client_test: - /builds/milmove/mymove/jest-junit-reports after_script: - *announce_failure - rules: - - *check_client_ignore_branch + # we want to make this run on every branch bc webhooks don't exist currently + # rules: + # - *check_client_ignore_branch client_test_coverage: stage: test diff --git a/Dockerfile b/Dockerfile index 18f55ac4c63..293ea88db39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:stable AS build-env +FROM harbor.csde.caci.com/docker.io/debian:stable AS build-env COPY config/tls/dod-wcf-root-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-root-ca-1.pem.crt COPY config/tls/dod-wcf-intermediate-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-intermediate-ca-1.pem.crt @@ -8,7 +8,7 @@ RUN apt-get install -y ca-certificates --no-install-recommends RUN update-ca-certificates # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY bin/rds-ca-rsa4096-g1.pem /bin/rds-ca-rsa4096-g1.pem diff --git a/Dockerfile.dp3 b/Dockerfile.dp3 index bd32b33446e..c2cafd6bd07 100644 --- a/Dockerfile.dp3 +++ b/Dockerfile.dp3 @@ -1,7 +1,7 @@ -FROM debian:stable AS build-env +FROM harbor.csde.caci.com/docker.io/debian:stable AS build-env # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 #AWS GovCloud RDS cert COPY bin/rds-ca-rsa4096-g1.pem /bin/rds-ca-rsa4096-g1.pem @@ -32,4 +32,4 @@ ENTRYPOINT ["/bin/milmove"] CMD ["serve", "--logging-level=debug"] -EXPOSE 8080 +EXPOSE 8080 \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local index bcbcd46c6bc..225bb958ab1 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -20,7 +20,7 @@ RUN rm -f bin/milmove && make bin/milmove ######### # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 COPY --from=builder --chown=root:root /home/circleci/project/bin/rds-ca-rsa4096-g1.pem /bin/rds-ca-rsa4096-g1.pem COPY --from=builder --chown=root:root /home/circleci/project/bin/rds-ca-2019-root.pem /bin/rds-ca-2019-root.pem diff --git a/Dockerfile.migrations b/Dockerfile.migrations index 6f8284da332..5d4956df394 100644 --- a/Dockerfile.migrations +++ b/Dockerfile.migrations @@ -1,4 +1,4 @@ -FROM debian:stable AS build-env +FROM harbor.csde.caci.com/docker.io/debian:stable AS build-env COPY config/tls/dod-wcf-root-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-root-ca-1.pem.crt COPY config/tls/dod-wcf-intermediate-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-intermediate-ca-1.pem.crt @@ -9,7 +9,7 @@ RUN update-ca-certificates # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 COPY config/tls/dod-wcf-root-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-root-ca-1.pem.crt COPY config/tls/dod-wcf-intermediate-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-intermediate-ca-1.pem.crt diff --git a/Dockerfile.reviewapp b/Dockerfile.reviewapp index 2949f635093..beeb0112195 100644 --- a/Dockerfile.reviewapp +++ b/Dockerfile.reviewapp @@ -108,7 +108,7 @@ RUN set -x \ ######### # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 as milmove +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 as milmove COPY --from=server_builder /build/bin/rds-ca-2019-root.pem /bin/rds-ca-2019-root.pem COPY --from=server_builder /build/bin/milmove /bin/milmove diff --git a/Dockerfile.tasks b/Dockerfile.tasks index 1e8034642a8..d41ee54e808 100644 --- a/Dockerfile.tasks +++ b/Dockerfile.tasks @@ -1,4 +1,4 @@ -FROM debian:stable AS build-env +FROM harbor.csde.caci.com/docker.io/debian:stable AS build-env COPY config/tls/dod-wcf-root-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-root-ca-1.pem.crt COPY config/tls/dod-wcf-intermediate-ca-1.pem /usr/local/share/ca-certificates/dod-wcf-intermediate-ca-1.pem.crt @@ -8,7 +8,7 @@ RUN apt-get install -y ca-certificates --no-install-recommends RUN update-ca-certificates # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY config/tls/milmove-cert-bundle.p7b /config/tls/milmove-cert-bundle.p7b diff --git a/Dockerfile.tasks_dp3 b/Dockerfile.tasks_dp3 index b305b972913..72f71bdb971 100644 --- a/Dockerfile.tasks_dp3 +++ b/Dockerfile.tasks_dp3 @@ -1,5 +1,5 @@ # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 # Demo Environment Certs COPY config/tls/api.demo.dp3.us.chain.der.p7b /config/tls/api.demo.dp3.us.chain.der.p7b diff --git a/Dockerfile.tasks_local b/Dockerfile.tasks_local index 5e5243cd779..bb67bf94872 100644 --- a/Dockerfile.tasks_local +++ b/Dockerfile.tasks_local @@ -19,7 +19,7 @@ RUN rm -f bin/milmove-tasks && make bin/milmove-tasks ######### # hadolint ignore=DL3007 -FROM gcr.io/distroless/base-debian11@sha256:ac69aa622ea5dcbca0803ca877d47d069f51bd4282d5c96977e0390d7d256455 +FROM gcr.io/distroless/base-debian12@sha256:74ddbf52d93fafbdd21b399271b0b4aac1babf8fa98cab59e5692e01169a1348 COPY --from=builder --chown=root:root /home/circleci/project/config/tls/milmove-cert-bundle.p7b /config/tls/milmove-cert-bundle.p7b COPY --from=builder --chown=root:root /home/circleci/project/bin/rds-ca-2019-root.pem /bin/rds-ca-2019-root.pem diff --git a/Makefile b/Makefile index 6017c9b15f5..e2f29bde50e 100644 --- a/Makefile +++ b/Makefile @@ -32,15 +32,6 @@ DB_PORT_DEPLOYED_MIGRATIONS=5434 DB_PORT_DOCKER=5432 REDIS_PORT=6379 REDIS_PORT_DOCKER=6379 -ifdef CIRCLECI - DB_PORT_DEV=5432 - DB_PORT_TEST=5432 - UNAME_S := $(shell uname -s) - ifeq ($(UNAME_S),Linux) - LDFLAGS=-linkmode external -extldflags -static - endif -endif - ifdef GITLAB DB_PORT_DEV=5432 DB_PORT_TEST=5432 @@ -96,10 +87,10 @@ prereqs: ## Check that pre-requirements are installed, includes dependency scrip .PHONY: check_hosts check_hosts: .check_hosts.stamp ## Check that hosts are in the /etc/hosts file .check_hosts.stamp: scripts/check-hosts-file -ifndef CIRCLECI +ifndef GITLAB scripts/check-hosts-file else - @echo "Not checking hosts on CircleCI." + @echo "Not checking hosts on GitLab." endif touch .check_hosts.stamp @@ -112,10 +103,10 @@ check_go_version: .check_go_version.stamp ## Check that the correct Golang versi .PHONY: check_gopath check_gopath: .check_gopath.stamp ## Check that $GOPATH exists in $PATH .check_gopath.stamp: scripts/check-gopath go.sum # Make sure any go binaries rebuild if version possibly changes -ifndef CIRCLECI +ifndef GITLAB scripts/check-gopath else - @echo "No need to check go path on CircleCI." + @echo "No need to check go path on GitLab." endif touch .check_gopath.stamp @@ -344,7 +335,7 @@ swagger_generate: .swagger_build.stamp ## Check that the build files haven't bee # if API docs have changed, swagger regeneration will capture those # changes. On Circle CI, or if the user has set # SWAGGER_AUTOREBUILD, rebuild automatically without asking -ifdef CIRCLECI +ifdef GITLAB SWAGGER_AUTOREBUILD=1 endif SWAGGER_FILES = $(shell find swagger swagger-def -type f) @@ -476,16 +467,16 @@ server_test_coverage: db_test_reset db_test_migrate redis_reset server_test_cove .PHONY: redis_pull redis_pull: ## Pull redis image -ifdef CIRCLECI - @echo "Relying on CircleCI to setup redis." +ifdef GITLAB + @echo "Relying on GitLab to setup redis." else docker pull $(REDIS_DOCKER_CONTAINER_IMAGE) endif .PHONY: redis_destroy redis_destroy: ## Destroy Redis -ifdef CIRCLECI - @echo "Relying on CircleCI to setup redis." +ifdef GITLAB + @echo "Relying on GitLab to setup redis." else @echo "Destroying the ${REDIS_DOCKER_CONTAINER} docker redis container..." docker rm -f $(REDIS_DOCKER_CONTAINER) || echo "No Redis container" @@ -493,8 +484,8 @@ endif .PHONY: redis_run redis_run: redis_pull ## Run Redis -ifdef CIRCLECI - @echo "Relying on CircleCI to setup redis." +ifdef GITLAB + @echo "Relying on GitLab to setup redis." else @echo "Stopping the Redis brew service in case it's running..." brew services stop redis 2> /dev/null || true @@ -522,17 +513,17 @@ db_pull: ## Pull db image .PHONY: db_dev_destroy db_dev_destroy: ## Destroy Dev DB -ifndef CIRCLECI +ifndef GITLAB @echo "Destroying the ${DB_DOCKER_CONTAINER_DEV} docker database container..." docker rm -f $(DB_DOCKER_CONTAINER_DEV) || echo "No database container" rm -fr mnt/db_dev # delete mount directory if exists else - @echo "Relying on CircleCI's database setup to destroy the DB." + @echo "Relying on GitLab's database setup to destroy the DB." endif .PHONY: db_dev_start db_dev_start: ## Start Dev DB -ifndef CIRCLECI +ifndef GITLAB brew services stop postgresql 2> /dev/null || true @echo "Starting the ${DB_DOCKER_CONTAINER_DEV} docker database container..." # If running do nothing, if not running try to start, if can't start then run @@ -542,16 +533,16 @@ ifndef CIRCLECI -p $(DB_PORT_DEV):$(DB_PORT_DOCKER)\ $(DB_DOCKER_CONTAINER_IMAGE) else - @echo "Relying on CircleCI's database setup to start the DB." + @echo "Relying on GitLab's database setup to start the DB." endif .PHONY: db_dev_create db_dev_create: ## Create Dev DB -ifndef CIRCLECI +ifndef GITLAB @echo "Create the ${DB_NAME_DEV} database..." DB_NAME=postgres scripts/wait-for-db && DB_NAME=postgres psql-wrapper "CREATE DATABASE $(DB_NAME_DEV);" || true else - @echo "Relying on CircleCI's database setup to create the DB." + @echo "Relying on GitLab's database setup to create the DB." psql postgres://postgres:$(PGPASSWORD)@localhost:$(DB_PORT)?sslmode=disable -c 'CREATE DATABASE $(DB_NAME_DEV);' endif @@ -616,17 +607,17 @@ db_dev_bandwidth_up: check_app bin/generate-test-data db_dev_truncate ## Truncat .PHONY: db_deployed_migrations_destroy db_deployed_migrations_destroy: ## Destroy Deployed Migrations DB -ifndef CIRCLECI +ifndef GITLAB @echo "Destroying the ${DB_DOCKER_CONTAINER_DEPLOYED_MIGRATIONS} docker database container..." docker rm -f $(DB_DOCKER_CONTAINER_DEPLOYED_MIGRATIONS) || echo "No database container" rm -fr mnt/db_deployed_migrations # delete mount directory if exists else - @echo "Relying on CircleCI's database setup to destroy the DB." + @echo "Relying on GitLab's database setup to destroy the DB." endif .PHONY: db_deployed_migrations_start db_deployed_migrations_start: ## Start Deployed Migrations DB -ifndef CIRCLECI +ifndef GITLAB brew services stop postgresql 2> /dev/null || true endif @echo "Starting the ${DB_DOCKER_CONTAINER_DEPLOYED_MIGRATIONS} docker database container..." @@ -671,18 +662,18 @@ db_deployed_psql: ## Open PostgreSQL shell for Deployed Migrations DB .PHONY: db_test_destroy db_test_destroy: ## Destroy Test DB -ifndef CIRCLECI +ifndef GITLAB @echo "Destroying the ${DB_DOCKER_CONTAINER_TEST} docker database container..." docker rm -f $(DB_DOCKER_CONTAINER_TEST) || \ echo "No database container" else - @echo "Relying on CircleCI's database setup to destroy the DB." + @echo "Relying on GitLab's database setup to destroy the DB." psql postgres://postgres:$(PGPASSWORD)@localhost:$(DB_PORT_TEST)?sslmode=disable -c 'DROP DATABASE IF EXISTS $(DB_NAME_TEST);' endif .PHONY: db_test_start db_test_start: ## Start Test DB -ifndef CIRCLECI +ifndef GITLAB brew services stop postgresql 2> /dev/null || true @echo "Starting the ${DB_DOCKER_CONTAINER_TEST} docker database container..." docker start $(DB_DOCKER_CONTAINER_TEST) || \ @@ -694,17 +685,17 @@ ifndef CIRCLECI --mount type=tmpfs,destination=/var/lib/postgresql/data \ $(DB_DOCKER_CONTAINER_IMAGE) else - @echo "Relying on CircleCI's database setup to start the DB." + @echo "Relying on GitLab's database setup to start the DB." endif .PHONY: db_test_create db_test_create: ## Create Test DB -ifndef CIRCLECI +ifndef GITLAB @echo "Create the ${DB_NAME_TEST} database..." DB_NAME=postgres DB_PORT=$(DB_PORT_TEST) scripts/wait-for-db && \ createdb -p $(DB_PORT_TEST) -h $(DB_HOST) -U postgres $(DB_NAME_TEST) || true else - @echo "Relying on CircleCI's database setup to create the DB." + @echo "Relying on GitLab's database setup to create the DB." psql postgres://postgres:$(PGPASSWORD)@localhost:$(DB_PORT_TEST)?sslmode=disable -c 'CREATE DATABASE $(DB_NAME_TEST);' endif @@ -721,7 +712,7 @@ db_test_truncate: .PHONY: db_test_migrate_standalone db_test_migrate_standalone: bin/milmove ## Migrate Test DB directly -ifndef CIRCLECI +ifndef GITLAB @echo "Migrating the ${DB_NAME_TEST} database..." DB_DEBUG=0 DB_NAME=$(DB_NAME_TEST) DB_PORT=$(DB_PORT_TEST) bin/milmove migrate -p "file://migrations/${APPLICATION}/secure;file://migrations/${APPLICATION}/schema" -m "migrations/${APPLICATION}/migrations_manifest.txt" else @@ -1046,7 +1037,7 @@ run_prime_docker: ## Runs the docker that spins up the Prime API and data to tes # .PHONY: make_test -make_test: ## Test make targets not checked by CircleCI +make_test: ## Test make targets not checked by GitLab scripts/make-test # @@ -1074,7 +1065,7 @@ pretty: gofmt ## Run code through JS and Golang formatters npx prettier --write --loglevel warn "src/**/*.{js,jsx}" .PHONY: docker_circleci -docker_circleci: ## Run CircleCI container locally with project mounted +docker_circleci: ## Run GitLab container locally with project mounted docker run -it --pull=always --rm=true -v $(PWD):$(PWD) -w $(PWD) -e CIRCLECI=1 milmove/circleci-docker:milmove-app-3d9acdaa37c81a87b5fc1c6193a8e528dd56e4ed bash .PHONY: docker_local_ssh_server_with_password @@ -1288,4 +1279,4 @@ multi_branch: check_local_env clone_repo check_cloned_env success_message ## Set # ----- END SETUP MULTI BRANCH ----- # -default: help +default: help \ No newline at end of file diff --git a/cmd/prime-api-client/prime/create_mto_service_item.go b/cmd/prime-api-client/prime/create_mto_service_item.go index 4147b173fce..9c96f43e82c 100644 --- a/cmd/prime-api-client/prime/create_mto_service_item.go +++ b/cmd/prime-api-client/prime/create_mto_service_item.go @@ -39,6 +39,10 @@ type shuttleParams struct { Body primemessages.MTOServiceItemShuttle `json:"body"` } +type domesticShuttleParams struct { + Body primemessages.MTOServiceItemDomesticShuttle `json:"body"` +} + // InitCreateMTOServiceItemFlags initializes flags. func InitCreateMTOServiceItemFlags(flag *pflag.FlagSet) { flag.String(utils.FilenameFlag, "", "Name of the file being passed in") @@ -146,12 +150,17 @@ func CreateMTOServiceItem(cmd *cobra.Command, args []string) error { var params shuttleParams err = utils.DecodeJSONFileToPayload(filename, utils.ContainsDash(args), ¶ms) serviceItemParams.SetBody(¶ms.Body) + case primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: + var params domesticShuttleParams + err = utils.DecodeJSONFileToPayload(filename, utils.ContainsDash(args), ¶ms) + serviceItemParams.SetBody(¶ms.Body) default: err = fmt.Errorf("allowed modelType(): %v", []primemessages.MTOServiceItemModelType{ primemessages.MTOServiceItemModelTypeMTOServiceItemDestSIT, primemessages.MTOServiceItemModelTypeMTOServiceItemOriginSIT, primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticCrating, primemessages.MTOServiceItemModelTypeMTOServiceItemShuttle, + primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle, }) } // return any decoding errors diff --git a/migrations/app/migrations_manifest.txt b/migrations/app/migrations_manifest.txt index 8613ca2bcf5..8f36174c2e6 100644 --- a/migrations/app/migrations_manifest.txt +++ b/migrations/app/migrations_manifest.txt @@ -1075,13 +1075,19 @@ 20250103130619_revert_data_change_for_gbloc_for_ak.up.sql 20250103142533_update_postal_codes_and_gblocs_for_ak.up.sql 20250103180420_update_pricing_proc_to_use_local_price_variable.up.sql +20250106202424_update_duty_locs.up.sql +20250109194140_create_audit_history_table_for_payment_service_items.up.sql 20250110001339_update_nts_release_enum_name.up.sql 20250110153428_add_shipment_address_updates_to_move_history.up.sql 20250110214012_homesafeconnect_cert.up.sql 20250113152050_rename_ubp.up.sql 20250113160816_updating_create_accessorial_service_item_proc.up.sql 20250113201232_update_estimated_pricing_procs_add_is_peak_func.up.sql +20250114164752_add_ppm_estimated_incentive_proc.up.sql 20250116200912_disable_homesafe_stg_cert.up.sql 20250120144247_update_pricing_proc_to_use_110_percent_weight.up.sql +20250120214107_add_international_ntsr_service_items.up.sql 20250121153007_update_pricing_proc_to_handle_international_shuttle.up.sql +20250121184450_upd_duty_loc_B-22242.up.sql 20250123173216_add_destination_queue_db_func_and_gbloc_view.up.sql +20250206173204_add_hawaii_data.up.sql diff --git a/migrations/app/schema/20250106202424_update_duty_locs.up.sql b/migrations/app/schema/20250106202424_update_duty_locs.up.sql new file mode 100644 index 00000000000..d3bc09560e5 --- /dev/null +++ b/migrations/app/schema/20250106202424_update_duty_locs.up.sql @@ -0,0 +1,51 @@ +--update duty location for NAS Meridian, MS to use zip 39309 +update duty_locations set name = 'NAS Meridian, MS 39309', address_id = '691551c2-71fe-4a15-871f-0c46dff98230' where id = '334fecaf-abeb-49ce-99b5-81d69c8beae5'; + +--remove 39302 duty location +delete from duty_locations where id = 'e55be32c-bf89-4927-8893-4454a26bfd55'; + +--update duty location for Minneapolis, MN 55460 to use 55467 +update orders set new_duty_location_id = 'fc4d669f-594a-4784-9831-bf2eb9f8948b' where new_duty_location_id = '4c960096-1fbc-4b9d-b7d9-5979a3ba7344'; + +--remove 55460 duty location +delete from duty_locations where id = '4c960096-1fbc-4b9d-b7d9-5979a3ba7344'; + +--add 92135 duty location +DO $$ +BEGIN + + INSERT INTO addresses + (id, street_address_1, street_address_2, city, state, postal_code, created_at, updated_at, street_address_3, county, is_oconus, country_id, us_post_region_cities_id) + SELECT '3d617fab-bf6f-4f07-8ab5-f7652b8e7f3e'::uuid, 'n/a', NULL, 'NAS N ISLAND', 'CA', '39125', now(), now(), NULL, 'SAN DIEGO', false, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, 'ce42858c-85af-4566-bbef-6b9aaf75c18a'::uuid + WHERE NOT EXISTS (select * from addresses where id = '3d617fab-bf6f-4f07-8ab5-f7652b8e7f3e'); + + INSERT INTO addresses + (id, street_address_1, street_address_2, city, state, postal_code, created_at, updated_at, street_address_3, county, is_oconus, country_id, us_post_region_cities_id) + SELECT '8d613f71-b80e-4ad4-95e7-00781b084c7c'::uuid, 'n/a', NULL, 'NAS NORTH ISLAND', 'CA', '39125', now(), now(), NULL, 'SAN DIEGO', false, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, '191165db-d30a-414d-862b-54afdfc7aeb9'::uuid + WHERE NOT EXISTS (select * from addresses where id = '8d613f71-b80e-4ad4-95e7-00781b084c7c'); + + INSERT INTO duty_locations (id,"name",affiliation,address_id,created_at,updated_at,transportation_office_id,provides_services_counseling) + SELECT '56255626-bbbe-4834-8324-1c08f011f2f6'::uuid,'NAS N Island, CA 92135',NULL,'3d617fab-bf6f-4f07-8ab5-f7652b8e7f3e'::uuid,now(),now(),null,true + WHERE NOT EXISTS (select * from duty_locations where id = '56255626-bbbe-4834-8324-1c08f011f2f6'); + + INSERT INTO duty_locations (id,"name",affiliation,address_id,created_at,updated_at,transportation_office_id,provides_services_counseling) + SELECT '7156098f-13cf-4455-bcd5-eb829d57c714'::uuid,'NAS North Island, CA 92135',NULL,'8d613f71-b80e-4ad4-95e7-00781b084c7c'::uuid,now(),now(),null,true + WHERE NOT EXISTS (select * from duty_locations where id = '7156098f-13cf-4455-bcd5-eb829d57c714'); +END $$; + +--add Cannon AFB 88101 duty location +DO $$ +BEGIN + + INSERT INTO addresses + (id, street_address_1, street_address_2, city, state, postal_code, created_at, updated_at, street_address_3, county, is_oconus, country_id, us_post_region_cities_id) + SELECT 'fb90a7df-6494-4974-a0ce-4bdbcaff80c0'::uuid, 'n/a', NULL, 'CANNON AFB', 'NM', '88101', now(), now(), NULL, 'CURRY', false, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, '68393e10-1aed-4a51-85a0-559a0a5b0e3f'::uuid + WHERE NOT EXISTS (select * from addresses where id = 'fb90a7df-6494-4974-a0ce-4bdbcaff80c0'); + + INSERT INTO duty_locations (id,"name",affiliation,address_id,created_at,updated_at,transportation_office_id,provides_services_counseling) + SELECT '98beab3c-f8ce-4e3c-b78e-8db614721621'::uuid, 'Cannon AFB, NM 88101',null, 'fb90a7df-6494-4974-a0ce-4bdbcaff80c0'::uuid,now(),now(),'80796bc4-e494-4b19-bb16-cdcdba187829',true + WHERE NOT EXISTS (select * from duty_locations where id = '98beab3c-f8ce-4e3c-b78e-8db614721621'); +END $$; + +--associate New London, CT duty location to New London transportation office +update duty_locations set transportation_office_id = '5eb485ae-fb9c-4c90-80e4-6231158797df' where id = '3a2a84cd-0991-4f40-9a19-f977608d08f0'; \ No newline at end of file diff --git a/migrations/app/schema/20250109194140_create_audit_history_table_for_payment_service_items.up.sql b/migrations/app/schema/20250109194140_create_audit_history_table_for_payment_service_items.up.sql new file mode 100644 index 00000000000..8318441ef8d --- /dev/null +++ b/migrations/app/schema/20250109194140_create_audit_history_table_for_payment_service_items.up.sql @@ -0,0 +1 @@ +SELECT add_audit_history_table(target_table := 'payment_service_items', audit_rows := BOOLEAN 't', audit_query_text := BOOLEAN 't', ignored_cols := ARRAY['created_at', 'updated_at', 'denied_at', 'requested_at', 'sent_to_gex_at', 'approved_at']); \ No newline at end of file diff --git a/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql b/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql new file mode 100644 index 00000000000..c0d7b3c9407 --- /dev/null +++ b/migrations/app/schema/20250114164752_add_ppm_estimated_incentive_proc.up.sql @@ -0,0 +1,249 @@ +-- IDFSIT PerUnitCents +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('fb7925e7-ebfe-49d9-9cf4-7219e68ec686'::uuid,'bd6064ca-e780-4ab4-a37b-0ae98eebb244','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IDASIT PerUnitCents +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('51393ee1-f505-4f7b-96c4-135f771af814'::uuid,'806c6d59-57ff-4a3f-9518-ebf29ba9cb10','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IOFSIT PerUnitCents +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('7518ec84-0c40-4c17-86dd-3ce04e2fe701'::uuid,'b488bf85-ea5e-49c8-ba5c-e2fa278ac806','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- IOASIT PerUnitCents +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('cff34123-e2a5-40ed-9cf3-451701850a26'::uuid,'bd424e45-397b-4766-9712-de4ae3a2da36','597bb77e-0ce7-4ba2-9624-24300962625f','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',false); -- PerUnitCents + +-- inserting PortZip param for FSC +-- we need this for international PPMs since they only get reimbursed for the CONUS -> Port portion +INSERT INTO service_params (id,service_id,service_item_param_key_id,created_at,updated_at,is_optional) VALUES + ('bb53e034-80c2-420e-8492-f54d2018fff1'::uuid,'4780b30c-e846-437a-b39a-c499a6b09872','d9ad3878-4b94-4722-bbaf-d4b8080f339d','2024-01-17 15:55:50.041957','2024-01-17 15:55:50.041957',true); -- PortZip + +-- remove PriceAreaIntlOrigin, we don't need it +DELETE FROM service_params +WHERE service_item_param_key_id = '6d44624c-b91b-4226-8fcd-98046e2f433d'; + +DELETE FROM service_item_param_keys +WHERE key = 'PriceAreaIntlOrigin'; + +-- remove PriceAreaIntlDest, we don't need it +DELETE FROM service_params +WHERE service_item_param_key_id = '4736f489-dfda-4df1-a303-8c434a120d5d'; + +DELETE FROM service_item_param_keys +WHERE key = 'PriceAreaIntlDest'; + +-- adding port info that PPMs will consume +INSERT INTO public.ports +(id, port_code, port_type, port_name, created_at, updated_at) +VALUES('d8776c6b-bc5e-45d8-ac50-ab60c34c022d'::uuid, '4E1', 'S','TACOMA, PUGET SOUND', now(), now()); + +INSERT INTO public.port_locations +(id, port_id, cities_id, us_post_region_cities_id, country_id, is_active, created_at, updated_at) +VALUES('ee3a97dc-112e-4805-8518-f56f2d9c6cc6'::uuid, 'd8776c6b-bc5e-45d8-ac50-ab60c34c022d'::uuid, 'baaf6ab1-6142-4fb7-b753-d0a142c75baf'::uuid, '86fef297-d61f-44ea-afec-4f679ce686b7'::uuid, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, true, now(), now()); + +-- func to fetch a service id from re_services by providing the service code +CREATE OR REPLACE FUNCTION get_service_id(service_code TEXT) RETURNS UUID AS $$ +DECLARE + service_id UUID; +BEGIN + SELECT rs.id INTO service_id FROM re_services rs WHERE rs.code = service_code; + IF service_id IS NULL THEN + RAISE EXCEPTION 'Service code % not found in re_services', service_code; + END IF; + RETURN service_id; +END; +$$ LANGUAGE plpgsql; + + +-- db func that will calculate a PPM's incentives +-- this is used for estimated/final/max incentives +CREATE OR REPLACE FUNCTION calculate_ppm_incentive( + ppm_id UUID, + pickup_address_id UUID, + destination_address_id UUID, + move_date DATE, + mileage INT, + weight INT, + is_estimated BOOLEAN, + is_actual BOOLEAN, + is_max BOOLEAN +) RETURNS TABLE ( + total_incentive NUMERIC, + price_islh NUMERIC, + price_ihpk NUMERIC, + price_ihupk NUMERIC, + price_fsc NUMERIC +) AS +$$ +DECLARE + ppm RECORD; + contract_id UUID; + o_rate_area_id UUID; + d_rate_area_id UUID; + service_id UUID; + estimated_fsc_multiplier NUMERIC; + fuel_price NUMERIC; + price_difference NUMERIC; + cents_above_baseline NUMERIC; +BEGIN + + IF NOT is_estimated AND NOT is_actual AND NOT is_max THEN + RAISE EXCEPTION 'is_estimated, is_actual, and is_max cannot all be FALSE. No update will be performed.'; + END IF; + + -- validating it's a real PPM + SELECT ppms.id INTO ppm FROM ppm_shipments ppms WHERE ppms.id = ppm_id; + IF ppm IS NULL THEN + RAISE EXCEPTION 'PPM with ID % not found', ppm_id; + END IF; + + contract_id := get_contract_id(move_date); + IF contract_id IS NULL THEN + RAISE EXCEPTION 'Contract not found for date: %', move_date; + END IF; + + o_rate_area_id := get_rate_area_id(pickup_address_id, NULL, contract_id); + IF o_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Origin rate area is NULL for address ID %', pickup_address_id; + END IF; + + d_rate_area_id := get_rate_area_id(destination_address_id, NULL, contract_id); + IF d_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Destination rate area is NULL for address ID %', destination_address_id; + END IF; + + -- ISLH calculation + service_id := get_service_id('ISLH'); + price_islh := ROUND( + calculate_escalated_price( + o_rate_area_id, + d_rate_area_id, + service_id, + contract_id, + 'ISLH', + move_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + + -- IHPK calculation + service_id := get_service_id('IHPK'); + price_ihpk := ROUND( + calculate_escalated_price( + o_rate_area_id, + NULL, + service_id, + contract_id, + 'IHPK', + move_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + + -- IHUPK calculation + service_id := get_service_id('IHUPK'); + price_ihupk := ROUND( + calculate_escalated_price( + NULL, + d_rate_area_id, + service_id, + contract_id, + 'IHUPK', + move_date + ) * (weight / 100)::NUMERIC * 100, 0 + ); + + -- FSC calculation + estimated_fsc_multiplier := get_fsc_multiplier(weight); + fuel_price := get_fuel_price(move_date); + price_difference := calculate_price_difference(fuel_price); + cents_above_baseline := mileage * estimated_fsc_multiplier; + price_fsc := ROUND((cents_above_baseline * price_difference) * 100); + + total_incentive := price_islh + price_ihpk + price_ihupk + price_fsc; + + UPDATE ppm_shipments + SET estimated_incentive = CASE WHEN is_estimated THEN total_incentive ELSE estimated_incentive END, + final_incentive = CASE WHEN is_actual THEN total_incentive ELSE final_incentive END, + max_incentive = CASE WHEN is_max THEN total_incentive ELSE max_incentive END + WHERE id = ppm_id; + + -- returning a table so we can use this data in the breakdown for the service member + RETURN QUERY SELECT total_incentive, price_islh, price_ihpk, price_ihupk, price_fsc; +END; +$$ LANGUAGE plpgsql; + + +-- db func that will calculate a PPM's SIT cost +-- returns a table with total cost and the cost of each first day/add'l day SIT service item +CREATE OR REPLACE FUNCTION calculate_ppm_sit_cost( + ppm_id UUID, + address_id UUID, + is_origin BOOLEAN, + move_date DATE, + weight INT, + sit_days INT +) RETURNS TABLE ( + total_cost INT, + price_first_day INT, + price_addl_day INT +) AS +$$ +DECLARE + ppm RECORD; + contract_id UUID; + sit_rate_area_id UUID; + service_id UUID; +BEGIN + -- make sure we validate parameters + IF sit_days IS NULL OR sit_days < 0 THEN + RAISE EXCEPTION 'SIT days must be a positive integer. Provided value: %', sit_days; + END IF; + + SELECT ppms.id INTO ppm FROM ppm_shipments ppms WHERE ppms.id = ppm_id; + IF ppm IS NULL THEN + RAISE EXCEPTION 'PPM with ID % not found', ppm_id; + END IF; + + contract_id := get_contract_id(move_date); + IF contract_id IS NULL THEN + RAISE EXCEPTION 'Contract not found for date: %', move_date; + END IF; + + sit_rate_area_id := get_rate_area_id(address_id, NULL, contract_id); + IF sit_rate_area_id IS NULL THEN + RAISE EXCEPTION 'Rate area is NULL for address ID % and contract ID %', address_id, contract_id; + END IF; + + -- calculate first day SIT cost + service_id := get_service_id(CASE WHEN is_origin THEN 'IOFSIT' ELSE 'IDFSIT' END); + price_first_day := ( + calculate_escalated_price( + CASE WHEN is_origin THEN sit_rate_area_id ELSE NULL END, + CASE WHEN NOT is_origin THEN sit_rate_area_id ELSE NULL END, + service_id, + contract_id, + CASE WHEN is_origin THEN 'IOFSIT' ELSE 'IDFSIT' END, + move_date + ) * (weight / 100)::NUMERIC * 100 + )::INT; + + -- calculate additional day SIT cost + service_id := get_service_id(CASE WHEN is_origin THEN 'IOASIT' ELSE 'IDASIT' END); + price_addl_day := ( + calculate_escalated_price( + CASE WHEN is_origin THEN sit_rate_area_id ELSE NULL END, + CASE WHEN NOT is_origin THEN sit_rate_area_id ELSE NULL END, + service_id, + contract_id, + CASE WHEN is_origin THEN 'IOASIT' ELSE 'IDASIT' END, + move_date + ) * (weight / 100)::NUMERIC * 100 * sit_days + )::INT; + + -- add em up + total_cost := price_first_day + price_addl_day; + + RETURN QUERY SELECT total_cost, price_first_day, price_addl_day; +END; +$$ LANGUAGE plpgsql; + diff --git a/migrations/app/schema/20250120214107_add_international_ntsr_service_items.up.sql b/migrations/app/schema/20250120214107_add_international_ntsr_service_items.up.sql new file mode 100644 index 00000000000..c80906403d9 --- /dev/null +++ b/migrations/app/schema/20250120214107_add_international_ntsr_service_items.up.sql @@ -0,0 +1,40 @@ +-- +-- Add service items for international NTS-R shipments. +-- +INSERT INTO re_service_items +(id, service_id, shipment_type, market_code, is_auto_approved, created_at, updated_at, sort) +VALUES + --ISLH International Shipping & Linehaul + ('bf76fb0f-408a-4391-8aa7-92908f3c027a', '9f3d551a-0725-430e-897e-80ee9add3ae9' ,'HHG_OUTOF_NTS', 'i', true, now(), now(), 1), + --PODFSC International POD Fuel Surcharge + ('db2106c8-887c-4304-aad2-c7413de13cc4', '388115e8-abe9-441d-96cf-a39f24baa0a3' ,'HHG_OUTOF_NTS', 'i', true, now(), now(), 2), + --POEFSC International POD Fuel Surcharge + ('509a491d-cddd-476c-9ba0-65077cf93e58', 'f75758d8-2fcd-40ba-9432-3ff3032a71d1' ,'HHG_OUTOF_NTS', 'i', true, now(), now(), 2), + --IHUPK International HHG unpack + ('89c6c283-4666-46c6-91ee-8041db7f88a7', '56e91c2d-015d-4243-9657-3ed34867abaa' ,'HHG_OUTOF_NTS', 'i', true, now(), now(), 3), + --INPK International NTS packing + ('4d348ec0-a278-4038-b061-6a4e17ea6721', '874cb86a-bc39-4f57-a614-53ee3fcacf14' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --ICRT International crating + ('b8f4e434-0912-44c5-b824-c60e5c5dffee', '86203d72-7f7c-49ff-82f0-5b95e4958f60' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IDASIT International destination add'l day SIT + ('7135540f-602c-4d02-ba66-403b7252738e', '806c6d59-57ff-4a3f-9518-ebf29ba9cb10' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IDDSIT International destination SIT delivery + ('5d3261a5-a7af-4133-b2e0-2a06f694c551', '28389ee1-56cf-400c-aa52-1501ecdd7c69' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IDFSIT International destination 1st day SIT + ('90e022da-9944-4563-98a3-6eb7cabb017e', 'bd6064ca-e780-4ab4-a37b-0ae98eebb244' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IDSHUT International destination shuttle service + ('ba6c218b-dd99-4ef4-87ed-421581218bbf', '22fc07ed-be15-4f50-b941-cbd38153b378' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IOASIT International origin add'l day SIT + ('ded116f5-ca9d-465e-acd8-3eee899e9713', 'bd424e45-397b-4766-9712-de4ae3a2da36' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IOFSIT International origin 1st day SIT + ('03114a6f-22ef-4664-9269-b76636466285', 'b488bf85-ea5e-49c8-ba5c-e2fa278ac806' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IOPSIT International origin SIT pickup + ('03e7b2fd-431d-4ce6-a640-78de846095c9', '6f4f6e31-0675-4051-b659-89832259f390' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IOSHUT International origin shuttle service + ('94b1786f-86ec-4736-8dbc-6a0e29b64272', '624a97c5-dfbf-4da9-a6e9-526b4f95af8d' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IUCRT International uncrating + ('cefe3094-5670-41c3-b6cd-72730bbb8fc7', '4132416b-b1aa-42e7-98f2-0ac0a03e8a31' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IOFSC International Origin SIT Fuel Surcharge + ('6506987f-925d-473d-9872-b94e0279c1af', '81e29d0c-02a6-4a7a-be02-554deb3ee49e' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL), + --IDSFSC International Destination SIT Fuel Surcharge + ('ca34445f-3e42-4e7e-b631-b4ae81c813c9', '690a5fc1-0ea5-4554-8294-a367b5daefa9' ,'HHG_OUTOF_NTS', 'i', false, now(), now(), NULL); diff --git a/migrations/app/schema/20250121184450_upd_duty_loc_B-22242.up.sql b/migrations/app/schema/20250121184450_upd_duty_loc_B-22242.up.sql new file mode 100644 index 00000000000..c21c04f6a81 --- /dev/null +++ b/migrations/app/schema/20250121184450_upd_duty_loc_B-22242.up.sql @@ -0,0 +1,111 @@ +DO $$ +BEGIN + + --remove duty loc Johnston City, TN 37602 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = 'd3a1be10-dcd7-4720-bcbe-7ba76d243687') THEN + + + update orders set origin_duty_location_id = 'cd0c7325-15bb-45c7-a690-26c56c903ed7' where origin_duty_location_id = 'd3a1be10-dcd7-4720-bcbe-7ba76d243687'; + update orders set new_duty_location_id = 'cd0c7325-15bb-45c7-a690-26c56c903ed7' where new_duty_location_id = 'd3a1be10-dcd7-4720-bcbe-7ba76d243687'; + + delete from duty_locations where id = 'd3a1be10-dcd7-4720-bcbe-7ba76d243687'; + + END IF; + +END $$; + +DO $$ +BEGIN + + --remove duty loc Oceanside, CA 92052 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = '54ca99b7-3c2a-42b0-aa1a-ad071ac580de') THEN + + update orders set origin_duty_location_id = 'a6993e7b-4600-44b9-b288-04ca011143f0' where origin_duty_location_id = '54ca99b7-3c2a-42b0-aa1a-ad071ac580de'; + update orders set new_duty_location_id = 'a6993e7b-4600-44b9-b288-04ca011143f0' where new_duty_location_id = '54ca99b7-3c2a-42b0-aa1a-ad071ac580de'; + + delete from duty_locations where id = '54ca99b7-3c2a-42b0-aa1a-ad071ac580de'; + + END IF; + +END $$; + +DO $$ +BEGIN + + --remove duty loc Albuquerque, NM 87103 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = '2cc57072-19fa-438b-a44b-e349dff11763') THEN + + update orders set new_duty_location_id = '54acfb0e-222b-49eb-b94b-ccb00c6f529c' where new_duty_location_id = '2cc57072-19fa-438b-a44b-e349dff11763'; + + delete from duty_locations where id = '2cc57072-19fa-438b-a44b-e349dff11763'; + + END IF; + +END $$; + +DO $$ +BEGIN + + --remove duty loc August, GA 30917 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = '109ac405-47fb-4e1e-9efb-58290453ac09') THEN + + update orders set origin_duty_location_id = '595363c2-14ee-48e0-b318-e76ab0016453' where origin_duty_location_id = '109ac405-47fb-4e1e-9efb-58290453ac09'; + update orders set new_duty_location_id = '595363c2-14ee-48e0-b318-e76ab0016453' where new_duty_location_id = '109ac405-47fb-4e1e-9efb-58290453ac09'; + + delete from duty_locations where id = '109ac405-47fb-4e1e-9efb-58290453ac09'; + + END IF; + +END $$; + +DO $$ +BEGIN + + --remove duty loc Frankfort, KY 40602 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = 'c7fadaa2-902f-4302-a7cd-108c525b96d4') THEN + + update orders set origin_duty_location_id = '1a973257-cd15-42a9-86be-a14796c014bc' where origin_duty_location_id = 'c7fadaa2-902f-4302-a7cd-108c525b96d4'; + update orders set new_duty_location_id = '1a973257-cd15-42a9-86be-a14796c014bc' where new_duty_location_id = 'c7fadaa2-902f-4302-a7cd-108c525b96d4'; + + delete from duty_locations where id = 'c7fadaa2-902f-4302-a7cd-108c525b96d4'; + + END IF; + +END $$; + +DO $$ +BEGIN + + --remove duty loc Seattle, WA 98111 + IF EXISTS (SELECT 1 FROM duty_locations WHERE id = '2fb3e898-d6de-4be7-8576-7c7b10c2a706') THEN + + update orders set origin_duty_location_id = 'e7fdae4f-6be7-4264-99f8-03ee8541499c' where origin_duty_location_id = '2fb3e898-d6de-4be7-8576-7c7b10c2a706'; + update orders set new_duty_location_id = 'e7fdae4f-6be7-4264-99f8-03ee8541499c' where new_duty_location_id = '2fb3e898-d6de-4be7-8576-7c7b10c2a706'; + + delete from duty_locations where id = '2fb3e898-d6de-4be7-8576-7c7b10c2a706'; + + END IF; + +END $$; + +--add Joint Base Lewis McChord, WA 98438 duty location +INSERT INTO public.addresses + (id, street_address_1, city, state, postal_code, created_at, updated_at, county, is_oconus, country_id, us_post_region_cities_id) +SELECT '23d3140b-1ba2-400f-9d57-317034673c06'::uuid, 'n/a', 'JOINT BASE LEWIS MCCHORD', 'WA', '98438', now(),now(), 'PIERCE', false, '791899e6-cd77-46f2-981b-176ecb8d7098'::uuid, '81182dd4-1693-4b8d-9b6f-042bc4254019'::uuid +WHERE NOT EXISTS (SELECT * FROM addresses WHERE id = '23d3140b-1ba2-400f-9d57-317034673c06'); + +INSERT INTO public.duty_locations +(id, "name", affiliation, address_id, created_at, updated_at, transportation_office_id, provides_services_counseling) +SELECT '38fc6718-b80f-4761-a077-cfa62e414e27', 'Joint Base Lewis McChord, WA 98438', 'AIR_FORCE', '23d3140b-1ba2-400f-9d57-317034673c06'::uuid, now(), now(), '95abaeaa-452f-4fe0-9264-960cd2a15ccd', true +WHERE NOT EXISTS (SELECT * FROM duty_locations WHERE id = '38fc6718-b80f-4761-a077-cfa62e414e27'); + +INSERT INTO public.duty_locations +(id, "name", affiliation, address_id, created_at, updated_at, transportation_office_id, provides_services_counseling) +SELECT '693781f4-d011-4925-a492-aa1185f3f1fe'::uuid, 'McChord AFB, WA 98438', 'AIR_FORCE', 'cc7894e3-148e-4e21-98df-37e45f0b2c9f'::uuid, now(), now(), '95abaeaa-452f-4fe0-9264-960cd2a15ccd', true +WHERE NOT EXISTS (SELECT * FROM duty_locations WHERE id = '693781f4-d011-4925-a492-aa1185f3f1fe'); + +--associate duty loc Yuma, AZ 85365 to transportation office PPPO DMO MCAS Yuma - USMC +update duty_locations set transportation_office_id = '6ac7e595-1e0c-44cb-a9a4-cd7205868ed4' where id = '9e94208a-881d-47bc-82c0-4f375471751e'; + +--update name for Alameda +update transportation_offices set name = 'PPSO Base Alameda - USCG' where id = '3fc4b408-1197-430a-a96a-24a5a1685b45'; \ No newline at end of file diff --git a/migrations/app/schema/20250206173204_add_hawaii_data.up.sql b/migrations/app/schema/20250206173204_add_hawaii_data.up.sql new file mode 100644 index 00000000000..718a3de7a6d --- /dev/null +++ b/migrations/app/schema/20250206173204_add_hawaii_data.up.sql @@ -0,0 +1,364 @@ +-- insert Hawaii rate areas +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('f041907c-080c-4e27-886f-4af7a2b8c559','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6e3f3e59-adf9-404d-9a27-5c556593f02d'::uuid,now(),now(),true), + ('49bff3e2-d795-4fb2-8061-6f7f7dc871f3','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'64b913ec-97f7-403d-b3de-b52da877947b'::uuid,now(),now(),true), + ('e3ff1ee1-2d39-4fac-9bb4-b5355030ba99','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'3c7bc566-7627-474a-a7e2-8b61c00383c1'::uuid,now(),now(),true), + ('a25de140-fac9-4303-9d83-769ed4205668','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'93d86d63-1f55-4ed8-8db0-6f19e5f6599b'::uuid,now(),now(),true), + ('b68a62a1-69e0-4a15-bcbf-8d31c4464fbd','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4ddb8033-4877-4e88-9e84-606e23b7131e'::uuid,now(),now(),true), + ('35387756-cea0-402b-a95c-be54e0094496','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'30d784ec-eded-4883-855c-2000d630774e'::uuid,now(),now(),true), + ('ff0167d9-ef93-43c8-9495-ba5bed8a0f69','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'563b5084-b7bc-4b37-8406-8c2dbf09cec4'::uuid,now(),now(),true), + ('956270a4-f850-4d94-b361-54d7104c381e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e82091dd-f4d5-40dc-ac78-73ce3dbba151'::uuid,now(),now(),true), + ('02c23633-7ee1-443a-9ecf-a1e422a28e6c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'0556a422-ebdf-4041-906f-eebb3679b5af'::uuid,now(),now(),true), + ('3e45d573-1df1-4d43-a5f0-45844bff3185','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'99cbe9bd-4b8e-4ef9-ada1-a9b3b41d38f8'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('6c26fd7c-44f5-40cf-ac88-e3202c752884','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'84e4594d-b21e-4b40-9ed0-37d6d332f55b'::uuid,now(),now(),true), + ('a52e2e8c-6251-4a94-9f3d-ecf8d18d097f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'d87f4b53-70dd-432b-996d-6b4de56cd579'::uuid,now(),now(),true), + ('49f630f7-de16-40be-b15a-aa8eee7a0a09','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6d4680f2-bd63-4af9-9abf-b5d4f5682423'::uuid,now(),now(),true), + ('8e854111-4805-46c6-92a2-aa7673a94b41','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e7c8daed-ad79-4c28-aa7f-7f348df929c7'::uuid,now(),now(),true), + ('9a41d67f-10b2-48d8-8e3c-2f20d6744dc2','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1719c3b4-e99f-4ac4-a5d0-2126aab7cd8c'::uuid,now(),now(),true), + ('76a161e0-40ee-4e6c-8138-8c78cefc0b73','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e40bea96-389c-44f9-8ab4-407208ca7828'::uuid,now(),now(),true), + ('4e0ef4d5-124a-4f20-b2a4-f6d0d31bf8eb','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9a297e12-fb3e-4454-b689-74fa82638c08'::uuid,now(),now(),true), + ('ed1aa4d4-99fd-4a2a-b757-c9d06e04484b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4c7443d1-25db-42db-99a7-cc08873e288b'::uuid,now(),now(),true), + ('7ff933f8-cede-4bf0-a918-2403d6b902a5','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4e312da4-f0db-4dec-ab96-6bc6531f3ce6'::uuid,now(),now(),true), + ('bdf97313-d164-4d9b-bec3-9aff052fa094','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5ca476cf-16f7-4ffa-b339-cfb0eff9f21d'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('d6d7872d-1751-44d0-86e1-217e7e129afe','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'de3d79a4-bb38-400d-b478-0fb5389fa6b5'::uuid,now(),now(),true), + ('1100147f-9baf-4a12-879e-dd40aadf073b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'91a847de-4602-42ff-8602-e6ba64fbe06f'::uuid,now(),now(),true), + ('c0abc8fa-f8c6-48e5-a49f-cc5714f6f509','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fc58a7c3-81ed-4362-b18b-2a3a798db953'::uuid,now(),now(),true), + ('9ba33b41-15de-4743-b8f8-75367d0126e7','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4f7347b7-441a-493d-bc70-ea421589c374'::uuid,now(),now(),true), + ('3188e86d-944a-42c1-b3b5-3bf7e360fb71','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1bd23977-93cc-4441-bb3d-5e46deb33be4'::uuid,now(),now(),true), + ('fc281a01-a7cd-41a0-86d9-716ab4864c08','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4b9ea4cb-5feb-4226-851e-086418dfe224'::uuid,now(),now(),true), + ('fb6d3db2-4079-48fe-9a67-170e200c9a8d','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'640de451-a487-4d24-9c8b-6405f1431a38'::uuid,now(),now(),true), + ('3bb35faa-229f-4721-82a2-becc9c98ae04','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'97508ed1-fde5-422c-b5ca-f7350bac5b72'::uuid,now(),now(),true), + ('313905e6-f237-4e1d-ab01-be6dfdddfa92','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'0d09ac18-0ee4-4905-879f-8b2d6b5112f7'::uuid,now(),now(),true), + ('0ed0a302-5016-4ccd-a6ba-5b28258af8dc','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'24baeda8-d21b-437f-bd43-8da4d6241011'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('846616d3-34e1-4304-b778-3bf8b3052c32','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'affb1936-8b01-41e5-bb7d-558476520092'::uuid,now(),now(),true), + ('f3b6f9da-4bfd-4ac5-a9e2-b4973adf372c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'87a96b8b-cc4b-416c-a0ab-f31868e90796'::uuid,now(),now(),true), + ('b42c8d98-71b3-464f-b868-6ab743ad7d3a','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6779b457-5e01-40bd-a584-76602c234bf0'::uuid,now(),now(),true), + ('41e52c48-4b52-4498-b898-22af6fcd5452','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'74792dae-6e22-4dc2-8185-6fc56fad958c'::uuid,now(),now(),true), + ('036d9855-ae02-4836-9405-f332d3654838','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9b8e278b-6a25-4bdf-831a-cea032d0f647'::uuid,now(),now(),true), + ('5bad5f3a-861a-448d-8747-66bfc77b9859','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'f9dfe9b6-fe0e-405a-b88f-4e085a940843'::uuid,now(),now(),true), + ('c7132b2f-8e28-4ee3-af04-d6137f14944a','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'274a633f-5cd7-4429-accb-56e77c0b9ac2'::uuid,now(),now(),true), + ('05775275-180d-4562-8cd5-9b20403e0824','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'cb071d07-649b-422e-99d4-a52ba1dbdccf'::uuid,now(),now(),true), + ('5c8029b9-85c3-452b-95cf-00d3d66bda7a','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'f0a8fb3b-7e36-43bc-9c23-488ea507c42b'::uuid,now(),now(),true), + ('b08f70e8-702b-454a-bc58-b0b10e1b603e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'8e667d68-60dc-4b13-87b5-0e7d4ac6b71b'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('20b866a6-d146-439b-b87b-d22412cda747','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1e3fd71e-0ac7-4fe3-9cf3-3d040473a10b'::uuid,now(),now(),true), + ('5dde8586-790a-40d6-9a46-e6cd725d1d5e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'d64af573-bfcb-4c39-bc32-c89632c26913'::uuid,now(),now(),true), + ('fedaa7ae-99a0-4981-9a07-835c0d094e1c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'712527f7-731f-4421-9b1c-9c58a64654b0'::uuid,now(),now(),true), + ('73bf3243-c29b-4b4e-9341-5aaf99616e56','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'8f87c288-1ed2-4bd5-a397-607585e22768'::uuid,now(),now(),true), + ('3c6ab532-1a18-4f78-9b95-deb1905d2d34','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'70934bce-e2fe-47d9-bb99-55f1e3c4fc00'::uuid,now(),now(),true), + ('af3a2ad4-585b-4947-bb40-3ae575013ed5','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fa790c20-9da9-45f5-af4c-11438f4fb735'::uuid,now(),now(),true), + ('01e44612-45b9-4964-9de9-843e7a3c0044','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e169dc8c-bb76-4290-9282-3aa4189c8970'::uuid,now(),now(),true), + ('6a0ce7ea-2daa-4f06-95bf-3c6222c5a8b3','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'af62f613-8e38-4178-9e79-020136b1fe76'::uuid,now(),now(),true), + ('e164aac9-e9aa-4831-8ef1-98660215705b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'481ec192-416e-49f0-ba7b-f565d5986eb3'::uuid,now(),now(),true), + ('670a550c-7243-4318-b8d4-e4efcfa32539','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5a821f96-fb5d-48dc-8eeb-642d3ed6f3ef'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('e25c2146-7ade-4552-a66d-d3bc948f39c0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'87048c04-81cb-4ef4-a536-2fcff23eee0a'::uuid,now(),now(),true), + ('d32022a5-f00b-461b-8a3b-0c69df06a94f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7133a5d8-0c5d-429b-b43f-36308a712b72'::uuid,now(),now(),true), + ('66861383-84f0-46c0-9597-c8765152601d','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'64fbd65a-c742-48f0-9342-b5e1340cffb6'::uuid,now(),now(),true), + ('9b52d7a7-8ebe-4cef-a49d-2e03eb87878c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'07a388d8-2a3d-43fe-baa6-91282a179617'::uuid,now(),now(),true), + ('05c41080-8f3d-41bc-8350-25c9c50dc8bf','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9f70610f-4a16-4636-ac8d-1a068075ef30'::uuid,now(),now(),true), + ('97b37859-0f6c-48ff-bec0-403cca95712f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7b121c37-3133-4daa-af24-26ad005b15d8'::uuid,now(),now(),true), + ('f0ae37b7-bcc1-4107-bdc7-43d21605d0c6','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'d12a6235-1399-453e-afd3-00fd9cf7faff'::uuid,now(),now(),true), + ('de8e5560-246b-45bb-a1df-0e9a0f0ab7ad','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'d6448f66-ae16-4ab3-af2a-7988e65c2c45'::uuid,now(),now(),true), + ('de85bbee-084e-42d2-ab68-677a927edb2a','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'8cab537e-1a01-49dc-9b44-c1dbe2226047'::uuid,now(),now(),true), + ('733e38ca-86e1-454f-87a4-884f8b17df21','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'2929ef2d-4373-4880-8017-85e8863afa04'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('8b2f2560-f26c-43d9-98ca-322a3e73b0bb','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'a62608f7-f9e0-4272-a051-4bd7014f9484'::uuid,now(),now(),true), + ('f5b7df58-dd68-4d24-8f7a-907895dbb151','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'ca858eba-ed95-4478-8ea2-ed5c99b75259'::uuid,now(),now(),true), + ('d7843a4e-0c70-466b-b01d-5909b1f62c11','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'462d1ef8-0d79-4e0b-b09c-ff349796c6b8'::uuid,now(),now(),true), + ('8d211696-7337-4c65-be73-9a88049476e4','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'cdb689fd-8000-4445-adb4-70c0941fe601'::uuid,now(),now(),true), + ('3821b597-e99a-432d-afa0-1aa74b802b94','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'10d1be45-6fdf-43a3-bfd3-930e493ff8fd'::uuid,now(),now(),true), + ('5918a6de-11ae-43f1-ba68-23cd71da6e7b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'0b5abf65-b844-4b1e-88cf-17c7469bb2a0'::uuid,now(),now(),true), + ('76e8e46c-ab0d-4c67-95b6-d00185dd2832','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9689a42b-27ad-4c44-88ee-af317acaee70'::uuid,now(),now(),true), + ('7b33ae55-5463-4b57-97e6-a7443a1e0904','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fa0344ac-7814-4c97-b22e-68721c0686a9'::uuid,now(),now(),true), + ('766fcf2c-9145-4f79-b502-d0ffaf96ef17','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'b28b69f2-3139-434a-8647-5ecb68b52d62'::uuid,now(),now(),true), + ('e1e1bf33-bd47-4ac7-9128-1f5c02d0db7e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fbb7608d-d1a2-4928-a57d-8fca190b1d05'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('8df33232-52a9-480f-afb8-df27cd099bd2','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'28e21fa2-10e5-421d-a744-f9a7941f5fe3'::uuid,now(),now(),true), + ('8415fff3-ccac-4ae8-82ac-e54f04ca4082','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1eb608e7-fd23-4d17-9731-6a48b69fd886'::uuid,now(),now(),true), + ('9d2ef3a3-9e55-4797-9b58-35429c3849a8','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5279030c-7331-4466-8d81-8ff94420a0cd'::uuid,now(),now(),true), + ('3e99cabc-75cd-4152-b519-cddfbd4294aa','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'61e3f4b5-66e1-449d-aab2-6a91a715cd01'::uuid,now(),now(),true), + ('616c1ea1-ac8f-4ccf-a28d-80078015ba0e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5ae811db-96b7-48c8-954e-d0252872668e'::uuid,now(),now(),true), + ('e33e15dd-d3aa-480d-a70c-8de943b1dccc','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5d3cb00b-2b90-47d1-b841-01ef70d334d6'::uuid,now(),now(),true), + ('1bdbc429-b5d6-4483-821e-a31839e63ad2','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'3829f26e-b286-4205-9f94-106d461425c9'::uuid,now(),now(),true), + ('0d327bc0-5315-44d1-848c-1f02667a81db','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'83f0986d-0f31-4870-8d43-d00d58f45aff'::uuid,now(),now(),true), + ('7088fd82-8a7f-47c3-a80b-b5255a795f6b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'848b1555-6920-4aa8-b44d-8fc4f08ab7a0'::uuid,now(),now(),true), + ('54045203-93a8-4011-a9b2-84673cc5176d','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7cea3c34-3f16-44ed-905d-af85ef732efb'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('f6b671cf-c344-46ba-bf68-08fcba2ebd84','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1ffe2a07-0271-4873-a98e-372e4d7ed4c6'::uuid,now(),now(),true), + ('26e71f17-7713-46d9-a059-5e07978b9b50','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6083c976-172c-4024-96b6-d1bd034352e9'::uuid,now(),now(),true), + ('f7c97962-b0f9-4671-b9d8-06c6195bcde0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'cf358ecf-fbc4-41b8-8872-1d8fc7b732d7'::uuid,now(),now(),true), + ('20e947f3-a7d1-423c-ae79-9886857d5dcc','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'253cefff-4e1a-4b20-9d21-05654e30132c'::uuid,now(),now(),true), + ('49e0f65d-1545-424d-b1c9-e5e3d0067fe9','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fa9bae80-8d1a-443a-902a-2e527a0d00ce'::uuid,now(),now(),true), + ('9f0d1cb8-4a5d-42f4-8766-c3ad7f029084','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'fd3c4223-7dcf-4efc-a80e-5aba50156140'::uuid,now(),now(),true), + ('e4392356-88c7-4841-a415-920aa9cdc7ca','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'24d4d08c-7676-4d05-aaf2-16e03a5d764f'::uuid,now(),now(),true), + ('f549cddf-ec33-4772-b259-5e12aa2f66b9','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7f3e06ad-b892-42c5-880e-a70fb9ba7c84'::uuid,now(),now(),true), + ('788cb14b-30e1-447d-95cc-358e29f01d8d','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'09e92a39-2407-4a64-891b-ed81f7d61048'::uuid,now(),now(),true), + ('46d6bc67-b8e5-4e11-834e-eaaa62cad46e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6895d267-67a1-4cde-8d46-e96d2142faf5'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('67d1f1ab-782a-452f-a607-ba2e6a9a87aa','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e5c12a89-96fa-4fd5-8448-ffb546c0ad82'::uuid,now(),now(),true), + ('51a17659-2fd7-49c3-9971-8fd1e341fcba','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6b63cd86-8dc5-498e-a541-782789aa6edd'::uuid,now(),now(),true), + ('646aaf79-c655-443d-a02f-81764d2965c0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'48c74f64-68a1-43fc-90e0-d9843d645b65'::uuid,now(),now(),true), + ('ecb1f2a7-525e-490a-8e88-b53a6681aa96','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'900cfeee-f5bb-41b3-976f-6a223d230b23'::uuid,now(),now(),true), + ('bf597a56-1728-4325-9734-1138799d6332','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e8283abd-d3bb-4d11-b66c-ea57d001fa7e'::uuid,now(),now(),true), + ('8608f8da-6f62-41ea-8c3d-024c1bcc7daa','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'8498acbd-27f2-4095-8a27-b01743bfa6e1'::uuid,now(),now(),true), + ('df227441-2e7f-456f-82c5-b4772c7cc5f6','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'d1dd9288-32ce-436f-800a-2f08bd1a9702'::uuid,now(),now(),true), + ('10177e21-4c50-44a6-956b-3eedeac77094','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9c6a995f-5cfc-4bcd-b1fb-bad1b4512fdf'::uuid,now(),now(),true), + ('373f38ab-0a7b-40c9-8cc5-0df01d1d87de','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'63b50a6f-a019-492a-84f4-3768f20bd5f2'::uuid,now(),now(),true), + ('14b7aa87-7a4e-4e47-b2e4-fa1ed8cdd2a8','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'2f870d5b-0d5a-4ccf-8f11-cab82d5cf1e6'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('05b15308-4fbd-4e03-ab59-437b9f37e69a','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'f24af83c-8734-48c0-9796-c944a36e137d'::uuid,now(),now(),true), + ('b36bffda-b020-4e49-bb6e-971d17116bde','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'384cf248-0a91-47e2-bab3-cdd6c3816843'::uuid,now(),now(),true), + ('89c8ec3e-af9d-4376-9b47-2a68589abcb4','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e7bbfe8c-ed10-4c50-a91b-9bf308f0a92a'::uuid,now(),now(),true), + ('ba61f387-aa0f-434c-aa61-5c15982d4ce4','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'592f383e-b566-4b0b-a8e7-cad06a61302f'::uuid,now(),now(),true), + ('09795f49-0694-4cfb-a711-8aa4edc64ef4','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'3098d832-1e05-485a-bdc2-75b249940c75'::uuid,now(),now(),true), + ('e54eb89b-88e8-469d-964e-faad60eafdef','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'b8204250-497a-4550-b2ac-cfa205acb252'::uuid,now(),now(),true), + ('19e2ad86-e55a-4ba5-bae1-f86e922933e3','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'698046a6-9284-4405-9abd-8a987dca3fc5'::uuid,now(),now(),true), + ('d1689b93-68e7-44d5-abd5-c3063bc4e670','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'27323f14-a2d8-4953-8bdc-ed68fcb9f5de'::uuid,now(),now(),true), + ('64bd4440-9ee4-4a80-bcbd-5701ab160e73','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'b668cee2-78c2-4065-9af0-400793b58262'::uuid,now(),now(),true), + ('00f9ac3e-b9c2-4cf5-af47-6b4799f7333f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'5cee3d06-1fb5-49b4-816f-63fa7b79719e'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('9771ac15-d640-4826-b055-c296696842e8','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'aef986d9-8f48-4ce7-b82a-d6fa1b5fb052'::uuid,now(),now(),true), + ('91b89a8b-81f9-4380-b94c-6f7a33f8b4d9','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'077558ed-d9ef-479f-8163-6fa629437697'::uuid,now(),now(),true), + ('5996d817-ab35-4477-8b77-832fe18a1abd','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1944531b-e876-4217-8c0f-ffc79e1ed814'::uuid,now(),now(),true), + ('f1795fad-656d-4600-ad09-05629eba7d43','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'526ce089-31f1-4648-aacb-757c1aa0a1ed'::uuid,now(),now(),true), + ('f899cc7f-96e6-4b26-af04-0d6edb9e01b2','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7abfb152-f97d-4004-b3a3-d7dcba9dabee'::uuid,now(),now(),true), + ('97425b50-7fab-4178-94c9-bf913585ac80','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'35c7c653-1c14-46c0-a485-7c487862acd4'::uuid,now(),now(),true), + ('70f5543b-82cb-4171-9c55-58e479ebaaa1','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'3ace2df3-045a-4a39-9a8a-364f96686af9'::uuid,now(),now(),true), + ('bae425c4-2ab4-4fdb-8b96-b0654894ebbc','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'ca928d85-d169-4e56-8412-840675dd08a8'::uuid,now(),now(),true), + ('736ab44f-3497-42f7-891d-845c868e7a29','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e17a9675-4d71-470e-8ee5-6f88605c0c2a'::uuid,now(),now(),true), + ('f118e38a-f46d-47d2-879d-c08b84781de8','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'069ecbb5-d5a3-4c19-af40-9a1bce621bb8'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('c74ddc03-0e4b-4645-99d2-7c711b3ab5c4','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'3689739b-c24e-4355-a612-6e3c3be7d16c'::uuid,now(),now(),true), + ('7b49bb85-c6bf-4cf3-8726-7256658e2ffb','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'b6713466-1a6c-4d57-901d-bef1323cd973'::uuid,now(),now(),true), + ('bf1b7d0b-6477-4784-9a62-aebb13921de9','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'018308a0-e7d0-43be-9c90-d13293102af5'::uuid,now(),now(),true), + ('2323d966-5d75-46df-b6df-eb43c616ba89','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'73be3c75-eb0f-4525-87c2-97d63494ca92'::uuid,now(),now(),true), + ('7607c8ed-de3f-414e-ae11-e85fca9bd847','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9fcecf37-9044-4c24-b2e1-9193a8bfec2d'::uuid,now(),now(),true), + ('c7e39bbe-048a-442a-a8c9-ec58de94caec','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'20528efa-9329-477f-bd8a-d4546abdce54'::uuid,now(),now(),true), + ('17a3c0ed-5cfb-4b60-8436-f0843e2d8624','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'71449390-b1fd-43ed-a72d-da805ff4fdbb'::uuid,now(),now(),true), + ('a3e5aad3-27d5-4e50-853d-504a3f15c08e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'928f17db-ff7d-46b8-a3b0-31587db2d334'::uuid,now(),now(),true), + ('c43a5bf3-0d45-4001-9565-b78a0c59c9d3','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'ad6ed769-a4d8-41a2-b9be-aa500546f8fa'::uuid,now(),now(),true), + ('919ed6b0-7939-4476-a7a8-6b49b820849c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'6dc3eafe-ee75-4ed9-9019-933dd155b1fc'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('43197d2c-728b-4204-8786-851da9fe9be8','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'17e33fff-b934-46d0-84a6-9f0a89ecd6f7'::uuid,now(),now(),true), + ('ec36f031-4c35-4e24-ba08-83e03846be85','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'244446bd-416c-4a6c-8d27-469db94417aa'::uuid,now(),now(),true), + ('64f29707-3503-40d0-96b7-6299381f1c60','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'c37d1d9e-cc49-4a98-9524-ac453e275c74'::uuid,now(),now(),true), + ('e7d88312-1da3-42b0-a487-2e2bf623842e','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'4be1d1fd-19b8-43c6-9f8f-3779f46fe518'::uuid,now(),now(),true), + ('ccb55592-003a-4135-802f-8d443422bc8b','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e9b38982-6164-47f9-b940-3c9e25951236'::uuid,now(),now(),true), + ('435c4a00-bc26-4cc3-8dd4-f2eb4e31c895','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'e474d48b-c435-4977-b290-63b40802d2d0'::uuid,now(),now(),true), + ('69cc680a-75a9-4b10-9847-51fbaf1caee1','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'910d037a-c995-418c-84ec-d460ac422ae3'::uuid,now(),now(),true), + ('391bd5cd-ab81-4eb0-b123-21a410c71cd5','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7497eff0-0583-4c95-9a29-5937527e1d68'::uuid,now(),now(),true), + ('d6ab4627-4d4f-41db-9baf-fec5f2487651','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'b0f6a629-8120-43b1-8a04-c9c9d741a39a'::uuid,now(),now(),true), + ('714c5707-aba2-455d-ad9a-dcf3b0b8b66c','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'632268db-5eb3-48fc-83c1-d6ab47b6a8a5'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('05554ee7-30d4-4bb3-bd8c-e0c478f4a037','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'9a1b9b7b-4319-4aee-b8aa-f0b98d94a528'::uuid,now(),now(),true), + ('f91a9353-16a2-4fbf-83db-54b06bc68381','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'0799c023-f508-4c68-a5b4-0654f002ab83'::uuid,now(),now(),true), + ('bb4c4e1e-828f-4a25-9dc2-81887e5553be','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'1841c945-89e8-414e-b570-864783247c28'::uuid,now(),now(),true), + ('fa941444-f49d-4f51-b025-d3c6b63242a7','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'80fcfb21-83f7-4738-91e9-69a435972474'::uuid,now(),now(),true), + ('eca65b4d-3981-46d9-9e10-a8d20c85c303','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'208b9fae-a5e7-4cc1-ba9e-15dceedee24b'::uuid,now(),now(),true), + ('cad3dad4-4025-478b-b3db-44b2f2886115','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7a0135b7-06bd-4c1e-849b-1d0201a886e2'::uuid,now(),now(),true), + ('f15f99d4-4a96-4603-8dbf-d3af72be8db0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'56ad34b3-961d-487e-a265-9f2cc4c58fec'::uuid,now(),now(),true), + ('5897d064-bbb0-4ac4-a283-5fa7b91cdfd6','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'a0846cf9-2ae8-44d1-ad1f-e4e80770e41c'::uuid,now(),now(),true), + ('bd4ca5db-cabb-44e7-8fdc-9a5d060161c7','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'749a1b35-b966-48ee-9be9-59ea581be394'::uuid,now(),now(),true), + ('4c286583-5039-4455-bc9f-36ed842f9aa5','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'470d34d6-da01-4fdf-949d-6b47ed2969d0'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('428a26d5-3dc7-44f6-8043-183b0bb60ee0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'a2df67b1-5b0b-4ab0-b9a0-11ff7ed8e0bf'::uuid,now(),now(),true), + ('86ff314b-0702-478a-aa19-c8cdb2f0fc2f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'80b7c3a1-f3ee-4e41-aeaa-ac1ab429d7ef'::uuid,now(),now(),true), + ('1e564ac9-c298-4a6e-b503-02db1459a621','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'345360d0-c3e9-4d87-b60b-59ab4b50c9ff'::uuid,now(),now(),true), + ('458174be-38b9-4acb-bd8b-7fb9012cd271','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'02bd132f-3620-4d55-ae47-d4a776b96235'::uuid,now(),now(),true), + ('0f86385c-8c84-4597-9c4e-3e8272c1df45','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'8e786192-31cf-4fcd-98c1-05de9a64d81b'::uuid,now(),now(),true), + ('83658135-4bdb-4d86-9359-6733fd7ef3ad','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'cd40843e-74d0-485f-ab78-c6c95d24b61a'::uuid,now(),now(),true), + ('c7e65080-7667-48b9-8794-013cdc1c72ec','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'f5a873a9-d1a0-4e6a-93fe-64cfdd0a957e'::uuid,now(),now(),true), + ('80f818a0-1047-466a-a3c1-ce2fc905f205','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'53d28592-dfbf-47d0-8543-1d45f206f8c0'::uuid,now(),now(),true), + ('13342500-620c-41db-af42-dcecf468ee43','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'550ed19a-fc56-42e7-a344-a5dc5c987037'::uuid,now(),now(),true), + ('c2c17c57-3d88-444d-b6ba-69a3d0d734f0','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'70f511da-6726-48a1-8991-4d33686419b7'::uuid,now(),now(),true); +INSERT INTO re_oconus_rate_areas (id,rate_area_id,country_id,us_post_region_cities_id, created_at,updated_at,active) VALUES + ('950daf0c-0174-4e2f-b03b-05c088520cff','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'7895e2e6-5877-465c-80b0-03da0c4bf3c2'::uuid,now(),now(),true), + ('9133dbe1-d08b-478e-8dbe-109fc05ca427','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'20b97576-a136-4ce3-a430-88ba601ee993'::uuid,now(),now(),true), + ('c1a175cd-c7d1-45ff-a49a-2a6cc4613e9f','71755cc7-0844-4523-a0ac-da9a1e743ad1'::uuid,'791899e6-cd77-46f2-981b-176ecb8d7098'::uuid,'f4d874b1-2445-439f-8c70-b10b8732fe11'::uuid,now(),now(),true); + + +-- insert Hawaii GBLOC associations +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('0b7f145f-0add-4aa3-bc51-a28a2f79b111','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f041907c-080c-4e27-886f-4af7a2b8c559'::uuid,NULL,NULL,true,now(),now()), + ('df2f72e1-29f6-4219-9255-16c9c6098fcf','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'49bff3e2-d795-4fb2-8061-6f7f7dc871f3'::uuid,NULL,NULL,true,now(),now()), + ('979afabc-346c-4481-b91c-eb1ed5061874','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e3ff1ee1-2d39-4fac-9bb4-b5355030ba99'::uuid,NULL,NULL,true,now(),now()), + ('9e294d37-1e1f-418a-9153-7a77ce5e6aaf','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'a25de140-fac9-4303-9d83-769ed4205668'::uuid,NULL,NULL,true,now(),now()), + ('a5f78d7f-f0a4-4190-829f-50bc0888a3f5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'b68a62a1-69e0-4a15-bcbf-8d31c4464fbd'::uuid,NULL,NULL,true,now(),now()), + ('28ca264b-f2e0-42fe-a4c8-d0a91045fea0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'35387756-cea0-402b-a95c-be54e0094496'::uuid,NULL,NULL,true,now(),now()), + ('1ba7ca65-ffbe-4049-897f-ef34b619f54d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ff0167d9-ef93-43c8-9495-ba5bed8a0f69'::uuid,NULL,NULL,true,now(),now()), + ('3bc8655a-3c05-4c96-b317-a2d21f52c5f1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'956270a4-f850-4d94-b361-54d7104c381e'::uuid,NULL,NULL,true,now(),now()), + ('39c53018-0418-4c19-8b66-334e7f689b45','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'02c23633-7ee1-443a-9ecf-a1e422a28e6c'::uuid,NULL,NULL,true,now(),now()), + ('feb1c2fa-b793-4029-8426-e9de4384d969','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3e45d573-1df1-4d43-a5f0-45844bff3185'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('d2c3e83d-a568-4b8e-9424-2f068c844066','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'6c26fd7c-44f5-40cf-ac88-e3202c752884'::uuid,NULL,NULL,true,now(),now()), + ('d1a64f83-a974-4741-a7a4-17c4747460d0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'a52e2e8c-6251-4a94-9f3d-ecf8d18d097f'::uuid,NULL,NULL,true,now(),now()), + ('58a79e8b-8dec-4de1-836b-c12bc987071a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'49f630f7-de16-40be-b15a-aa8eee7a0a09'::uuid,NULL,NULL,true,now(),now()), + ('078eae9d-9cdb-40fa-aa9b-788dd5391536','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8e854111-4805-46c6-92a2-aa7673a94b41'::uuid,NULL,NULL,true,now(),now()), + ('b4db50d9-8480-4914-9983-d1f7b06ae4e5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9a41d67f-10b2-48d8-8e3c-2f20d6744dc2'::uuid,NULL,NULL,true,now(),now()), + ('6851d85a-6bed-42ef-9287-66b6b7e02616','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'76a161e0-40ee-4e6c-8138-8c78cefc0b73'::uuid,NULL,NULL,true,now(),now()), + ('6adc1f68-be6c-438a-959b-396448a69917','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'4e0ef4d5-124a-4f20-b2a4-f6d0d31bf8eb'::uuid,NULL,NULL,true,now(),now()), + ('0d3e608e-8ab9-4be3-a0ee-b91f83ad916d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ed1aa4d4-99fd-4a2a-b757-c9d06e04484b'::uuid,NULL,NULL,true,now(),now()), + ('f30c24bf-9111-4ffe-acbe-e9bf53fe8ef3','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'7ff933f8-cede-4bf0-a918-2403d6b902a5'::uuid,NULL,NULL,true,now(),now()), + ('301e01f6-1b04-499c-9d5b-224271487551','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bdf97313-d164-4d9b-bec3-9aff052fa094'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('361645a1-3345-40ac-b03d-9a60828018a8','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'d6d7872d-1751-44d0-86e1-217e7e129afe'::uuid,NULL,NULL,true,now(),now()), + ('1a121ea0-1026-4904-a97d-efdc92bc2fab','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'1100147f-9baf-4a12-879e-dd40aadf073b'::uuid,NULL,NULL,true,now(),now()), + ('352d8604-a837-4731-a8d1-92082d8f5576','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c0abc8fa-f8c6-48e5-a49f-cc5714f6f509'::uuid,NULL,NULL,true,now(),now()), + ('b5dd300a-faa8-4ab7-8276-c9e7e5bf4506','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9ba33b41-15de-4743-b8f8-75367d0126e7'::uuid,NULL,NULL,true,now(),now()), + ('9450a528-7898-4b73-91a0-573257cf8288','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3188e86d-944a-42c1-b3b5-3bf7e360fb71'::uuid,NULL,NULL,true,now(),now()), + ('87f6667b-06f3-4beb-9570-2c6b320efb16','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'fc281a01-a7cd-41a0-86d9-716ab4864c08'::uuid,NULL,NULL,true,now(),now()), + ('1f7b2bf7-6bb5-4a80-bacf-4e726150358d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'fb6d3db2-4079-48fe-9a67-170e200c9a8d'::uuid,NULL,NULL,true,now(),now()), + ('f9a42eaa-a002-4159-a40d-2d45187c95dd','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3bb35faa-229f-4721-82a2-becc9c98ae04'::uuid,NULL,NULL,true,now(),now()), + ('28298fc7-f185-43fb-9f47-5e64717f7131','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'313905e6-f237-4e1d-ab01-be6dfdddfa92'::uuid,NULL,NULL,true,now(),now()), + ('7ef4d9e8-7647-488a-b803-2dd68a78a487','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'0ed0a302-5016-4ccd-a6ba-5b28258af8dc'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('e175dc40-d8d4-4f06-a362-ac50c4fe1890','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'846616d3-34e1-4304-b778-3bf8b3052c32'::uuid,NULL,NULL,true,now(),now()), + ('09125d7c-4192-45c4-b60e-1d34d2236dc1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f3b6f9da-4bfd-4ac5-a9e2-b4973adf372c'::uuid,NULL,NULL,true,now(),now()), + ('0d08e371-109d-4465-8185-3f3a9d992d14','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'b42c8d98-71b3-464f-b868-6ab743ad7d3a'::uuid,NULL,NULL,true,now(),now()), + ('cc097b12-3720-4b38-9819-32e1c66aae83','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'41e52c48-4b52-4498-b898-22af6fcd5452'::uuid,NULL,NULL,true,now(),now()), + ('5d7c1eff-2565-41e9-ae94-3792726606d7','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'036d9855-ae02-4836-9405-f332d3654838'::uuid,NULL,NULL,true,now(),now()), + ('59738b80-7b6d-4de5-a02f-a03f30e28a21','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5bad5f3a-861a-448d-8747-66bfc77b9859'::uuid,NULL,NULL,true,now(),now()), + ('5506f869-0bb8-4333-bd92-fb067257287e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c7132b2f-8e28-4ee3-af04-d6137f14944a'::uuid,NULL,NULL,true,now(),now()), + ('d085b83d-e5dd-4736-b2de-65024d9a770f','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'05775275-180d-4562-8cd5-9b20403e0824'::uuid,NULL,NULL,true,now(),now()), + ('3d6e0a5e-3f5d-46a5-859c-5d2681277de5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5c8029b9-85c3-452b-95cf-00d3d66bda7a'::uuid,NULL,NULL,true,now(),now()), + ('bf0fa527-ac28-4c28-937e-0c9ecd6e6b0a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'b08f70e8-702b-454a-bc58-b0b10e1b603e'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('bedd5f6b-0b3b-4cd8-8724-4c20757af6d0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'20b866a6-d146-439b-b87b-d22412cda747'::uuid,NULL,NULL,true,now(),now()), + ('0d4479de-6229-40ca-a8bf-a844ff6b7653','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5dde8586-790a-40d6-9a46-e6cd725d1d5e'::uuid,NULL,NULL,true,now(),now()), + ('104c0668-9649-4b05-9224-96677c4313d1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'fedaa7ae-99a0-4981-9a07-835c0d094e1c'::uuid,NULL,NULL,true,now(),now()), + ('2622a62b-bcf6-49df-a985-da16972b2565','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'73bf3243-c29b-4b4e-9341-5aaf99616e56'::uuid,NULL,NULL,true,now(),now()), + ('03d65ff1-3441-45c9-bb15-353af13566aa','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3c6ab532-1a18-4f78-9b95-deb1905d2d34'::uuid,NULL,NULL,true,now(),now()), + ('6e2a2921-cd88-4e0f-ba51-ba80a0d11f0b','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'af3a2ad4-585b-4947-bb40-3ae575013ed5'::uuid,NULL,NULL,true,now(),now()), + ('01ff6dc1-080a-44f6-b5dd-a2adbc817c72','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'01e44612-45b9-4964-9de9-843e7a3c0044'::uuid,NULL,NULL,true,now(),now()), + ('f772ac89-acf5-49a5-8539-a35ad5bb406c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'6a0ce7ea-2daa-4f06-95bf-3c6222c5a8b3'::uuid,NULL,NULL,true,now(),now()), + ('21407cef-1b02-46e1-add0-714241bb0965','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e164aac9-e9aa-4831-8ef1-98660215705b'::uuid,NULL,NULL,true,now(),now()), + ('82f0ed7b-9c68-41a4-ae6e-932c0aa037e5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'670a550c-7243-4318-b8d4-e4efcfa32539'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('b27b3565-7a34-4f76-bb9d-bd4764a3c046','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e25c2146-7ade-4552-a66d-d3bc948f39c0'::uuid,NULL,NULL,true,now(),now()), + ('f78103a7-62d4-4825-bd47-f2bf8a5f386a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'d32022a5-f00b-461b-8a3b-0c69df06a94f'::uuid,NULL,NULL,true,now(),now()), + ('4275485b-c5cd-443e-8eab-b25db7aca167','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'66861383-84f0-46c0-9597-c8765152601d'::uuid,NULL,NULL,true,now(),now()), + ('7ea4019d-19dc-4e0e-92e7-149772385368','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9b52d7a7-8ebe-4cef-a49d-2e03eb87878c'::uuid,NULL,NULL,true,now(),now()), + ('127ed1f9-05bc-401b-ace7-854662eda3e4','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'05c41080-8f3d-41bc-8350-25c9c50dc8bf'::uuid,NULL,NULL,true,now(),now()), + ('aa0a3c08-abb7-4d70-9b80-00eb525b7329','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'97b37859-0f6c-48ff-bec0-403cca95712f'::uuid,NULL,NULL,true,now(),now()), + ('a4d57e99-c781-4bfb-af80-902294551ca0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f0ae37b7-bcc1-4107-bdc7-43d21605d0c6'::uuid,NULL,NULL,true,now(),now()), + ('7cb0e3ef-d141-44ad-a6ff-32264d563cdb','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'de8e5560-246b-45bb-a1df-0e9a0f0ab7ad'::uuid,NULL,NULL,true,now(),now()), + ('01729d8d-c001-4a23-9e60-11c8c5d64cbe','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'de85bbee-084e-42d2-ab68-677a927edb2a'::uuid,NULL,NULL,true,now(),now()), + ('ff651ae1-ff18-4e0c-9080-145c444d873e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'733e38ca-86e1-454f-87a4-884f8b17df21'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('f130def4-2be3-4f26-81e8-a75381e52ad1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8b2f2560-f26c-43d9-98ca-322a3e73b0bb'::uuid,NULL,NULL,true,now(),now()), + ('111ff46a-e1cc-4eaf-9cd4-e33fc021d3d3','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f5b7df58-dd68-4d24-8f7a-907895dbb151'::uuid,NULL,NULL,true,now(),now()), + ('380e1bb4-0138-4a5b-badf-353a08cd58c5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'d7843a4e-0c70-466b-b01d-5909b1f62c11'::uuid,NULL,NULL,true,now(),now()), + ('71ef938f-769d-404d-9c2a-a48cf954d1e4','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8d211696-7337-4c65-be73-9a88049476e4'::uuid,NULL,NULL,true,now(),now()), + ('14774917-4376-4191-a925-23a5322154f2','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3821b597-e99a-432d-afa0-1aa74b802b94'::uuid,NULL,NULL,true,now(),now()), + ('9f42b8d1-0d5d-43a8-b7b1-bac5b91e6145','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5918a6de-11ae-43f1-ba68-23cd71da6e7b'::uuid,NULL,NULL,true,now(),now()), + ('ec6cea21-f2b3-4176-8abd-6179ec3f190e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'76e8e46c-ab0d-4c67-95b6-d00185dd2832'::uuid,NULL,NULL,true,now(),now()), + ('b3377895-ca73-482f-91c3-c986c22b545c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'7b33ae55-5463-4b57-97e6-a7443a1e0904'::uuid,NULL,NULL,true,now(),now()), + ('c97bb442-5839-4521-b95e-cfecbe4d0125','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'766fcf2c-9145-4f79-b502-d0ffaf96ef17'::uuid,NULL,NULL,true,now(),now()), + ('3305fccd-efac-4717-a1f6-1f31daf0a0a5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e1e1bf33-bd47-4ac7-9128-1f5c02d0db7e'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('e6508bd6-32eb-4f53-8405-57cdef069e78','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8df33232-52a9-480f-afb8-df27cd099bd2'::uuid,NULL,NULL,true,now(),now()), + ('671d7baa-8b6c-4b71-a3fc-166d138cfaaa','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8415fff3-ccac-4ae8-82ac-e54f04ca4082'::uuid,NULL,NULL,true,now(),now()), + ('14f91fd9-bc46-439d-b788-6b6553085f80','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9d2ef3a3-9e55-4797-9b58-35429c3849a8'::uuid,NULL,NULL,true,now(),now()), + ('614c349e-4450-45d6-b14c-86b9aa118c63','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'3e99cabc-75cd-4152-b519-cddfbd4294aa'::uuid,NULL,NULL,true,now(),now()), + ('481b28f2-994e-403e-9391-6352d3f0f0bd','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'616c1ea1-ac8f-4ccf-a28d-80078015ba0e'::uuid,NULL,NULL,true,now(),now()), + ('57eb8a31-2606-4d23-8a23-f7576069a23e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e33e15dd-d3aa-480d-a70c-8de943b1dccc'::uuid,NULL,NULL,true,now(),now()), + ('d7e38c2b-b1c2-4d22-9650-397266536d20','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'1bdbc429-b5d6-4483-821e-a31839e63ad2'::uuid,NULL,NULL,true,now(),now()), + ('2e270943-5d8b-445c-8545-8eeb1d8cb8c2','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'0d327bc0-5315-44d1-848c-1f02667a81db'::uuid,NULL,NULL,true,now(),now()), + ('7466d6e1-e3e8-46cc-a6c0-b6dcabea653a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'7088fd82-8a7f-47c3-a80b-b5255a795f6b'::uuid,NULL,NULL,true,now(),now()), + ('e3f744cf-8a28-4ffb-b177-71aecc96575d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'54045203-93a8-4011-a9b2-84673cc5176d'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('209d6646-d917-4e2f-a69a-9ae914cdd129','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f6b671cf-c344-46ba-bf68-08fcba2ebd84'::uuid,NULL,NULL,true,now(),now()), + ('8dfa8159-1ba0-44d1-a340-c3152dd4679d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'26e71f17-7713-46d9-a059-5e07978b9b50'::uuid,NULL,NULL,true,now(),now()), + ('fdaa6dde-ebc1-431c-93d9-65963dac2960','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f7c97962-b0f9-4671-b9d8-06c6195bcde0'::uuid,NULL,NULL,true,now(),now()), + ('5faa2cb2-8dff-4fca-83f9-b5ab22a7296a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'20e947f3-a7d1-423c-ae79-9886857d5dcc'::uuid,NULL,NULL,true,now(),now()), + ('3946aae4-063f-423d-9932-8b04ca15b931','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'49e0f65d-1545-424d-b1c9-e5e3d0067fe9'::uuid,NULL,NULL,true,now(),now()), + ('b5113518-3412-4062-a91a-775adbe277b6','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9f0d1cb8-4a5d-42f4-8766-c3ad7f029084'::uuid,NULL,NULL,true,now(),now()), + ('40063586-641d-447a-8932-6862103e0596','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e4392356-88c7-4841-a415-920aa9cdc7ca'::uuid,NULL,NULL,true,now(),now()), + ('8e33e711-ff66-4a2e-8201-7898ae73ea26','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f549cddf-ec33-4772-b259-5e12aa2f66b9'::uuid,NULL,NULL,true,now(),now()), + ('90440c26-70b7-4988-b1f7-f69d3abd5eb5','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'788cb14b-30e1-447d-95cc-358e29f01d8d'::uuid,NULL,NULL,true,now(),now()), + ('353b2723-2f0e-4623-8622-2e6071b0b403','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'46d6bc67-b8e5-4e11-834e-eaaa62cad46e'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('221a4c6c-8084-45c8-8ac3-2474e13eadff','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'67d1f1ab-782a-452f-a607-ba2e6a9a87aa'::uuid,NULL,NULL,true,now(),now()), + ('79d5bfe2-b124-4e81-9a07-b0169eff1b9d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'51a17659-2fd7-49c3-9971-8fd1e341fcba'::uuid,NULL,NULL,true,now(),now()), + ('2e102552-37d0-4c22-ae7d-b9e931b34dc1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'646aaf79-c655-443d-a02f-81764d2965c0'::uuid,NULL,NULL,true,now(),now()), + ('0198b34d-5528-4934-bf47-22024ed34e3d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ecb1f2a7-525e-490a-8e88-b53a6681aa96'::uuid,NULL,NULL,true,now(),now()), + ('016e0c1e-f890-4372-8968-a4da81c7be7c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bf597a56-1728-4325-9734-1138799d6332'::uuid,NULL,NULL,true,now(),now()), + ('47d78518-39ad-455f-8a44-fe974cce5c0e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'8608f8da-6f62-41ea-8c3d-024c1bcc7daa'::uuid,NULL,NULL,true,now(),now()), + ('03979a62-cf11-4ec4-840b-f3863701b2d0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'df227441-2e7f-456f-82c5-b4772c7cc5f6'::uuid,NULL,NULL,true,now(),now()), + ('880e545d-a6f1-46ce-a3d9-703a95c55b9c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'10177e21-4c50-44a6-956b-3eedeac77094'::uuid,NULL,NULL,true,now(),now()), + ('b9a799fb-fffe-4739-bc73-d2232fc08bb3','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'373f38ab-0a7b-40c9-8cc5-0df01d1d87de'::uuid,NULL,NULL,true,now(),now()), + ('f6d8da90-1a28-4dbc-9aca-ff3c1cf838db','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'14b7aa87-7a4e-4e47-b2e4-fa1ed8cdd2a8'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('910ddece-59c3-482c-ae9b-d23f3478a06b','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'05b15308-4fbd-4e03-ab59-437b9f37e69a'::uuid,NULL,NULL,true,now(),now()), + ('40db8097-6261-4507-9c87-371d8f4ac794','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'b36bffda-b020-4e49-bb6e-971d17116bde'::uuid,NULL,NULL,true,now(),now()), + ('b4bccff5-4836-43f7-9ab9-f8e05b65a528','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'89c8ec3e-af9d-4376-9b47-2a68589abcb4'::uuid,NULL,NULL,true,now(),now()), + ('953326d7-6244-47f0-ac10-a336445eadfe','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ba61f387-aa0f-434c-aa61-5c15982d4ce4'::uuid,NULL,NULL,true,now(),now()), + ('c02b62dc-e3be-447c-ae20-ef7d419a5757','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'09795f49-0694-4cfb-a711-8aa4edc64ef4'::uuid,NULL,NULL,true,now(),now()), + ('41ed482c-5381-48bb-acbb-f296e238f067','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e54eb89b-88e8-469d-964e-faad60eafdef'::uuid,NULL,NULL,true,now(),now()), + ('e1f11e3e-e8f4-4fbb-83e2-2a76bee64c24','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'19e2ad86-e55a-4ba5-bae1-f86e922933e3'::uuid,NULL,NULL,true,now(),now()), + ('25750162-53a8-45b1-9008-4b01cdf1afca','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'d1689b93-68e7-44d5-abd5-c3063bc4e670'::uuid,NULL,NULL,true,now(),now()), + ('38e423ae-f98f-409a-b662-1f1b90715b8c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'64bd4440-9ee4-4a80-bcbd-5701ab160e73'::uuid,NULL,NULL,true,now(),now()), + ('12425939-589e-4ebe-b67d-48dd2a086115','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'00f9ac3e-b9c2-4cf5-af47-6b4799f7333f'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('816cab83-2e6a-4643-81b8-e6698d68eab0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9771ac15-d640-4826-b055-c296696842e8'::uuid,NULL,NULL,true,now(),now()), + ('4653a73a-17e8-4e90-8f0d-19f40025c82e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'91b89a8b-81f9-4380-b94c-6f7a33f8b4d9'::uuid,NULL,NULL,true,now(),now()), + ('3780c556-4aa0-4ada-a5ef-3ab6c2f9c969','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5996d817-ab35-4477-8b77-832fe18a1abd'::uuid,NULL,NULL,true,now(),now()), + ('14abdfc1-7c4f-4b48-a60b-da7daf82e00d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f1795fad-656d-4600-ad09-05629eba7d43'::uuid,NULL,NULL,true,now(),now()), + ('51cc866b-4d6d-4c16-8b9f-6ebac7d7c45f','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f899cc7f-96e6-4b26-af04-0d6edb9e01b2'::uuid,NULL,NULL,true,now(),now()), + ('ffe1e0c3-648f-4cb6-a696-a165f31a6c1e','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'97425b50-7fab-4178-94c9-bf913585ac80'::uuid,NULL,NULL,true,now(),now()), + ('74b41148-97d2-4d61-8794-f93e119c9730','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'70f5543b-82cb-4171-9c55-58e479ebaaa1'::uuid,NULL,NULL,true,now(),now()), + ('8e733754-e830-476d-acde-c85a129c2704','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bae425c4-2ab4-4fdb-8b96-b0654894ebbc'::uuid,NULL,NULL,true,now(),now()), + ('96824049-1a8a-47a7-b9e3-1afd090741bf','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'736ab44f-3497-42f7-891d-845c868e7a29'::uuid,NULL,NULL,true,now(),now()), + ('09c670b5-220a-4ea6-8ddf-5baeea1ce9f2','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f118e38a-f46d-47d2-879d-c08b84781de8'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('ef4b04d4-b472-4105-b6c8-00c1f314ab17','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c74ddc03-0e4b-4645-99d2-7c711b3ab5c4'::uuid,NULL,NULL,true,now(),now()), + ('c9fc02c1-d1a7-48c5-b4eb-3e0598534820','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'7b49bb85-c6bf-4cf3-8726-7256658e2ffb'::uuid,NULL,NULL,true,now(),now()), + ('354777b7-8b85-4e4d-89bf-3152c75d7571','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bf1b7d0b-6477-4784-9a62-aebb13921de9'::uuid,NULL,NULL,true,now(),now()), + ('68562095-8f5c-4edf-ba21-29d99215ab11','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'2323d966-5d75-46df-b6df-eb43c616ba89'::uuid,NULL,NULL,true,now(),now()), + ('2d810fbe-c240-46f6-ba1e-792c4f25bb47','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'7607c8ed-de3f-414e-ae11-e85fca9bd847'::uuid,NULL,NULL,true,now(),now()), + ('5bece096-75d3-4f26-bf4e-561701fe7ce2','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c7e39bbe-048a-442a-a8c9-ec58de94caec'::uuid,NULL,NULL,true,now(),now()), + ('bf728fd7-dee8-428c-a753-c91d96dbf26b','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'17a3c0ed-5cfb-4b60-8436-f0843e2d8624'::uuid,NULL,NULL,true,now(),now()), + ('61e2274a-2bd7-422f-94bc-f2beeb1fb84b','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'a3e5aad3-27d5-4e50-853d-504a3f15c08e'::uuid,NULL,NULL,true,now(),now()), + ('a3a5fb63-aa87-464a-a439-7851fcbca270','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c43a5bf3-0d45-4001-9565-b78a0c59c9d3'::uuid,NULL,NULL,true,now(),now()), + ('24de42d0-4689-4d7d-b94c-8596ae12c7a6','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'919ed6b0-7939-4476-a7a8-6b49b820849c'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('e611b59a-bd7d-4a84-a12b-d4a96ac2ec47','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'43197d2c-728b-4204-8786-851da9fe9be8'::uuid,NULL,NULL,true,now(),now()), + ('c013b9b9-6d4e-40d9-88d2-332fcf421595','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ec36f031-4c35-4e24-ba08-83e03846be85'::uuid,NULL,NULL,true,now(),now()), + ('d649b940-429b-443f-b81c-385fb789ac64','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'64f29707-3503-40d0-96b7-6299381f1c60'::uuid,NULL,NULL,true,now(),now()), + ('5f70407d-8c97-43b8-aebf-8b876f55f2b2','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'e7d88312-1da3-42b0-a487-2e2bf623842e'::uuid,NULL,NULL,true,now(),now()), + ('b2f26584-e32d-4eb0-b721-f79e4b986666','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'ccb55592-003a-4135-802f-8d443422bc8b'::uuid,NULL,NULL,true,now(),now()), + ('0a291f21-8ec8-490e-aea3-21ddcaf155fa','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'435c4a00-bc26-4cc3-8dd4-f2eb4e31c895'::uuid,NULL,NULL,true,now(),now()), + ('edc42ed1-66dd-4d85-8db0-3ed3c8b9c694','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'69cc680a-75a9-4b10-9847-51fbaf1caee1'::uuid,NULL,NULL,true,now(),now()), + ('dbdcb0a7-2827-42b3-9ecf-beaaeeccc219','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'391bd5cd-ab81-4eb0-b123-21a410c71cd5'::uuid,NULL,NULL,true,now(),now()), + ('6a8697c6-cb7b-4fb7-95cc-acf6ad909193','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'d6ab4627-4d4f-41db-9baf-fec5f2487651'::uuid,NULL,NULL,true,now(),now()), + ('9d21922c-e067-4aa6-b8a1-0e0d0b30d1ec','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'714c5707-aba2-455d-ad9a-dcf3b0b8b66c'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('edcdda55-d1cc-4137-bcee-c35403795032','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'05554ee7-30d4-4bb3-bd8c-e0c478f4a037'::uuid,NULL,NULL,true,now(),now()), + ('34cd246e-f618-4204-b761-c6202da4e999','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f91a9353-16a2-4fbf-83db-54b06bc68381'::uuid,NULL,NULL,true,now(),now()), + ('4cb5c010-6c43-468a-88a7-3e1e4aad1e93','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bb4c4e1e-828f-4a25-9dc2-81887e5553be'::uuid,NULL,NULL,true,now(),now()), + ('56bed37c-d397-4405-a507-36e48b7be94c','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'fa941444-f49d-4f51-b025-d3c6b63242a7'::uuid,NULL,NULL,true,now(),now()), + ('0198017d-0822-4b77-adac-eb83690d3262','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'eca65b4d-3981-46d9-9e10-a8d20c85c303'::uuid,NULL,NULL,true,now(),now()), + ('97ce133d-103a-47d8-bdd6-f25b24e258fd','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'cad3dad4-4025-478b-b3db-44b2f2886115'::uuid,NULL,NULL,true,now(),now()), + ('bf618f8e-9047-4cb2-afc4-84c756084be7','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'f15f99d4-4a96-4603-8dbf-d3af72be8db0'::uuid,NULL,NULL,true,now(),now()), + ('7fe6d143-070b-48c5-b138-404386a53e79','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'5897d064-bbb0-4ac4-a283-5fa7b91cdfd6'::uuid,NULL,NULL,true,now(),now()), + ('e2321d3b-2648-4cc1-9651-f7b00ff5da11','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'bd4ca5db-cabb-44e7-8fdc-9a5d060161c7'::uuid,NULL,NULL,true,now(),now()), + ('a03bffce-29fe-48e3-baed-e7a42641d663','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'4c286583-5039-4455-bc9f-36ed842f9aa5'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('0069ba48-0a2a-47f5-b669-b055f95382c3','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'428a26d5-3dc7-44f6-8043-183b0bb60ee0'::uuid,NULL,NULL,true,now(),now()), + ('b4b6187e-e576-43dc-82bb-d2978b6b8e8d','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'86ff314b-0702-478a-aa19-c8cdb2f0fc2f'::uuid,NULL,NULL,true,now(),now()), + ('c51bb0b0-1d56-4dff-bb7c-a3bab342926b','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'1e564ac9-c298-4a6e-b503-02db1459a621'::uuid,NULL,NULL,true,now(),now()), + ('95d14542-3ea7-4263-a5a4-2fb9827ec6ed','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'458174be-38b9-4acb-bd8b-7fb9012cd271'::uuid,NULL,NULL,true,now(),now()), + ('1c872bd1-3d94-4976-9358-701c9ab76bd6','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'0f86385c-8c84-4597-9c4e-3e8272c1df45'::uuid,NULL,NULL,true,now(),now()), + ('1f6f5329-dde1-487b-8b2a-182f0a68e2c1','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'83658135-4bdb-4d86-9359-6733fd7ef3ad'::uuid,NULL,NULL,true,now(),now()), + ('6b861ecd-979b-4f11-b413-97f1ea253215','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c7e65080-7667-48b9-8794-013cdc1c72ec'::uuid,NULL,NULL,true,now(),now()), + ('1a9c0ae9-f5c4-48f3-bd49-1b1ff202be56','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'80f818a0-1047-466a-a3c1-ce2fc905f205'::uuid,NULL,NULL,true,now(),now()), + ('cf9c449f-e2b3-4716-ae41-48c9a1d605f0','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'13342500-620c-41db-af42-dcecf468ee43'::uuid,NULL,NULL,true,now(),now()), + ('71a21ed8-da0e-461c-9bf7-39945d06068a','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c2c17c57-3d88-444d-b6ba-69a3d0d734f0'::uuid,NULL,NULL,true,now(),now()); +INSERT INTO gbloc_aors (id,jppso_regions_id,oconus_rate_area_id,department_indicator,shipment_type,is_active,created_at,updated_at) VALUES + ('87a10170-ce98-4ddc-bb17-0a7996ae7385','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'950daf0c-0174-4e2f-b03b-05c088520cff'::uuid,NULL,NULL,true,now(),now()), + ('4073c7fa-822b-4f03-b4a6-213e600199ff','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'9133dbe1-d08b-478e-8dbe-109fc05ca427'::uuid,NULL,NULL,true,now(),now()), + ('6f8eacf4-dd13-491e-a3bd-4c1bc49d38bc','97611c2a-3ef6-4883-bf9a-3a7709f2acdd'::uuid,'c1a175cd-c7d1-45ff-a49a-2a6cc4613e9f'::uuid,NULL,NULL,true,now(),now()); diff --git a/pkg/assets/notifications/templates/move_counseled_template.html b/pkg/assets/notifications/templates/move_counseled_template.html index d8c22a243b0..cf60b2410de 100644 --- a/pkg/assets/notifications/templates/move_counseled_template.html +++ b/pkg/assets/notifications/templates/move_counseled_template.html @@ -2,6 +2,10 @@

This is a confirmation that your counselor has approved move details for the assigned move code{{if .Locator}} {{.Locator}}{{end}}{{if .OriginDutyLocation}} from {{.OriginDutyLocation}}{{end}}{{if .DestinationLocation}} to {{.DestinationLocation}}{{end}} in the MilMove system.

+{{if .WeightRestriction}} +

Your move has been identified as going to an administratively restricted HHG weight location. Your weight restriction is {{.WeightRestriction}}lbs.

+

Be advised, you may be required to pay excess cost if you choose to move more than your weight restriction.

+{{end}}

What this means to you:
If you are doing a Personally Procured Move (PPM), you can start moving your personal property.

diff --git a/pkg/assets/notifications/templates/move_counseled_template.txt b/pkg/assets/notifications/templates/move_counseled_template.txt index 39aead0ef8a..535c27f66e9 100644 --- a/pkg/assets/notifications/templates/move_counseled_template.txt +++ b/pkg/assets/notifications/templates/move_counseled_template.txt @@ -1,7 +1,9 @@ *** DO NOT REPLY directly to this email *** This is a confirmation that your counselor has approved move details for the assigned move code {{.Locator}}{{if .OriginDutyLocation}} from {{.OriginDutyLocation}}{{end}}{{if .DestinationLocation}} to {{.DestinationLocation}}{{end}} in the MilMove system. - +{{if .WeightRestriction}} +

Your move has been identified as going to an administratively restricted HHG weight location. Your weight restriction is {{.WeightRestriction}}lbs. Be advised, you may be required to pay excess cost if you choose to move more than your weight restriction.

+{{end}} What this means to you: If you are doing a Personally Procured Move (PPM), you can start moving your personal property. diff --git a/pkg/assets/notifications/templates/move_submitted_template.html b/pkg/assets/notifications/templates/move_submitted_template.html index cce7d87e8b8..a2a408af88d 100644 --- a/pkg/assets/notifications/templates/move_submitted_template.html +++ b/pkg/assets/notifications/templates/move_submitted_template.html @@ -20,10 +20,11 @@

- Your weight allowance: {{.WeightAllowance}} pounds. + Your standard weight allowance: {{.WeightAllowance}} pounds. That is how much combined weight the government will pay for all movements between authorized locations under your - orders. -

+ orders.

+

Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less.

+

If you move more than {{.WeightAllowance}} pounds or ship to/from an other than authorized location, you may owe the diff --git a/pkg/assets/notifications/templates/move_submitted_template.txt b/pkg/assets/notifications/templates/move_submitted_template.txt index 72f1c68566d..85516ead51f 100644 --- a/pkg/assets/notifications/templates/move_submitted_template.txt +++ b/pkg/assets/notifications/templates/move_submitted_template.txt @@ -6,7 +6,7 @@ We have assigned you a move code: {{.Locator}}. You can use this code when talki {{ if .OriginDutyLocationPhoneLine -}} To change any information about your move, or to add or cancel shipments, you should contact {{.OriginDutyLocationPhoneLine}} or visit your local transportation office ({{.OneSourceTransportationOfficeLink}}) . {{- end }} {{- if not .OriginDutyLocationPhoneLine }} To change any information about your move, or to add or cancel shipments, you should contact your nearest transportation office. You can find the contact information using the directory of PCS-related contacts ({{.OneSourceTransportationOfficeLink}}) . {{- end }} -Your weight allowance: {{.WeightAllowance}} pounds. That is how much combined weight the government will pay for all movements between authorized locations under your orders. +Your standard weight allowance: {{.WeightAllowance}} pounds. That is how much combined weight the government will pay for all movements between authorized locations under your orders. Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less. If you move more than {{.WeightAllowance}} pounds or ship to/from an other than authorized location, you may owe the government the difference in cost between what you are authorized and what you decide to move. diff --git a/pkg/assets/sql_scripts/move_history_fetcher.sql b/pkg/assets/sql_scripts/move_history_fetcher.sql index 28f88eab583..fc293e20377 100644 --- a/pkg/assets/sql_scripts/move_history_fetcher.sql +++ b/pkg/assets/sql_scripts/move_history_fetcher.sql @@ -52,14 +52,18 @@ WITH move AS ( 'closeout_office_name', (SELECT transportation_offices.name FROM transportation_offices WHERE transportation_offices.id = uuid(c.closeout_office_id)), 'counseling_office_name', - (SELECT transportation_offices.name FROM transportation_offices WHERE transportation_offices.id = uuid(c.counseling_transportation_office_id)) + (SELECT transportation_offices.name FROM transportation_offices WHERE transportation_offices.id = uuid(c.counseling_transportation_office_id)), + 'assigned_office_user_first_name', + (SELECT office_users.first_name FROM office_users WHERE office_users.id IN (uuid(c.sc_assigned_id), uuid(c.too_assigned_id), uuid(c.tio_assigned_id))), + 'assigned_office_user_last_name', + (SELECT office_users.last_name FROM office_users WHERE office_users.id IN (uuid(c.sc_assigned_id), uuid(c.too_assigned_id), uuid(c.tio_assigned_id))) )) )::TEXT AS context, NULL AS context_id FROM audit_history JOIN move ON audit_history.object_id = move.id - JOIN jsonb_to_record(audit_history.changed_data) as c(closeout_office_id TEXT, counseling_transportation_office_id TEXT) on TRUE + JOIN jsonb_to_record(audit_history.changed_data) as c(closeout_office_id TEXT, counseling_transportation_office_id TEXT, sc_assigned_id TEXT, too_assigned_id TEXT, tio_assigned_id TEXT) ON TRUE WHERE audit_history.table_name = 'moves' -- Remove log for when shipment_seq_num updates AND NOT (audit_history.event_name = NULL AND audit_history.changed_data::TEXT LIKE '%shipment_seq_num%' AND LENGTH(audit_history.changed_data::TEXT) < 25) @@ -212,7 +216,8 @@ WITH move AS ( 'shipment_id', move_shipments.id::TEXT, 'shipment_id_abbr', move_shipments.shipment_id_abbr, 'shipment_type', move_shipments.shipment_type, - 'shipment_locator', move_shipments.shipment_locator + 'shipment_locator', move_shipments.shipment_locator, + 'rejection_reason', payment_service_items.rejection_reason ) )::TEXT AS context, payment_requests.id AS id, @@ -239,6 +244,42 @@ WITH move AS ( JOIN move_payment_requests ON move_payment_requests.id = audit_history.object_id WHERE audit_history.table_name = 'payment_requests' ), + move_payment_service_items AS ( + SELECT + jsonb_agg(jsonb_build_object( + 'name', re_services.name, + 'price', payment_service_items.price_cents::TEXT, + 'status', payment_service_items.status, + 'rejection_reason', payment_service_items.rejection_reason, + 'paid_at', payment_service_items.paid_at, + 'shipment_id', move_shipments.id::TEXT, + 'shipment_id_abbr', move_shipments.shipment_id_abbr, + 'shipment_type', move_shipments.shipment_type, + 'shipment_locator', move_shipments.shipment_locator + ) + )::TEXT AS context, + payment_service_items.id AS id + FROM + payment_requests + JOIN payment_service_items ON payment_service_items.payment_request_id = payment_requests.id + JOIN move_service_items ON move_service_items.id = payment_service_items.mto_service_item_id + LEFT JOIN move_shipments ON move_shipments.id = move_service_items.mto_shipment_id + JOIN re_services ON move_service_items.re_service_id = re_services.id + WHERE + payment_requests.move_id = (SELECT move.id FROM move) + GROUP BY + payment_service_items.id + ), + payment_service_items_logs AS ( + SELECT DISTINCT + audit_history.*, + context AS context, + NULL AS context_id + FROM + audit_history + JOIN move_payment_service_items ON move_payment_service_items.id = audit_history.object_id + WHERE audit_history.table_name = 'payment_service_items' + ), move_proof_of_service_docs AS ( SELECT proof_of_service_docs.*, @@ -717,6 +758,11 @@ WITH move AS ( FROM payment_requests_logs UNION + SELECT + * + FROM + payment_service_items_logs + UNION SELECT * FROM diff --git a/pkg/factory/entitlement_factory.go b/pkg/factory/entitlement_factory.go index 17aff299990..0df88cd9ca4 100644 --- a/pkg/factory/entitlement_factory.go +++ b/pkg/factory/entitlement_factory.go @@ -98,4 +98,5 @@ func BuildEntitlement(db *pop.Connection, customs []Customization, traits []Trai } return entitlement + } diff --git a/pkg/factory/mto_shipment_factory.go b/pkg/factory/mto_shipment_factory.go index 0dc95a166e6..86819917c97 100644 --- a/pkg/factory/mto_shipment_factory.go +++ b/pkg/factory/mto_shipment_factory.go @@ -103,6 +103,10 @@ func buildMTOShipmentWithBuildType(db *pop.Connection, customs []Customization, newMTOShipment.StorageFacilityID = &storageFacility.ID } + if newMTOShipment.ShipmentType == models.MTOShipmentTypeHHGOutOfNTS && newMTOShipment.StorageFacility != nil { + newMTOShipment.PickupAddress = &newMTOShipment.StorageFacility.Address + } + if addPrimeActualWeight { actualWeight := unit.Pound(980) newMTOShipment.PrimeActualWeight = &actualWeight diff --git a/pkg/factory/ppm_shipment_factory.go b/pkg/factory/ppm_shipment_factory.go index 0306aebe547..cc87032285e 100644 --- a/pkg/factory/ppm_shipment_factory.go +++ b/pkg/factory/ppm_shipment_factory.go @@ -87,42 +87,60 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, ppmShipment.W2Address = &w2AddressResult } - oldDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.OriginDutyLocation.Address - pickupAddress := BuildAddress(db, []Customization{ - { - Model: models.Address{ - StreetAddress1: "987 New Street", - City: oldDutyLocationAddress.City, - State: oldDutyLocationAddress.State, - PostalCode: oldDutyLocationAddress.PostalCode, + pickupAddressResult := findValidCustomization(customs, Addresses.PickupAddress) + if pickupAddressResult != nil { + pickupAddressResultCustoms := convertCustomizationInList(customs, Addresses.PickupAddress, Address) + + pickupAddressResult := BuildAddress(db, pickupAddressResultCustoms, traits) + ppmShipment.PickupAddressID = &pickupAddressResult.ID + ppmShipment.PickupAddress = &pickupAddressResult + } else { + oldDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.OriginDutyLocation.Address + pickupAddress := BuildAddress(db, []Customization{ + { + Model: models.Address{ + StreetAddress1: "987 New Street", + City: oldDutyLocationAddress.City, + State: oldDutyLocationAddress.State, + PostalCode: oldDutyLocationAddress.PostalCode, + }, }, - }, - }, nil) - ppmShipment.PickupAddressID = &pickupAddress.ID - ppmShipment.PickupAddress = &pickupAddress + }, nil) + ppmShipment.PickupAddressID = &pickupAddress.ID + ppmShipment.PickupAddress = &pickupAddress + } - newDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.NewDutyLocation.Address - destinationAddress := BuildAddress(db, []Customization{ - { - Model: models.Address{ - StreetAddress1: "123 New Street", - City: newDutyLocationAddress.City, - State: newDutyLocationAddress.State, - PostalCode: newDutyLocationAddress.PostalCode, + deliveryAddressResult := findValidCustomization(customs, Addresses.DeliveryAddress) + if deliveryAddressResult != nil { + deliveryAddressResultCustoms := convertCustomizationInList(customs, Addresses.DeliveryAddress, Address) + + deliveryAddressResult := BuildAddress(db, deliveryAddressResultCustoms, traits) + ppmShipment.DestinationAddressID = &deliveryAddressResult.ID + ppmShipment.DestinationAddress = &deliveryAddressResult + } else { + newDutyLocationAddress := ppmShipment.Shipment.MoveTaskOrder.Orders.NewDutyLocation.Address + destinationAddress := BuildAddress(db, []Customization{ + { + Model: models.Address{ + StreetAddress1: "123 New Street", + City: newDutyLocationAddress.City, + State: newDutyLocationAddress.State, + PostalCode: newDutyLocationAddress.PostalCode, + }, }, - }, - }, nil) - ppmShipment.DestinationAddressID = &destinationAddress.ID - ppmShipment.DestinationAddress = &destinationAddress + }, nil) + ppmShipment.DestinationAddressID = &destinationAddress.ID + ppmShipment.DestinationAddress = &destinationAddress + } if buildType == ppmBuildFullAddress { secondaryPickupAddress := BuildAddress(db, []Customization{ { Model: models.Address{ StreetAddress1: "123 Main Street", - City: pickupAddress.City, - State: pickupAddress.State, - PostalCode: pickupAddress.PostalCode, + City: ppmShipment.PickupAddress.City, + State: ppmShipment.PickupAddress.State, + PostalCode: ppmShipment.PickupAddress.PostalCode, }, }, }, nil) @@ -130,9 +148,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "1234 Main Street", - City: destinationAddress.City, - State: destinationAddress.State, - PostalCode: destinationAddress.PostalCode, + City: ppmShipment.DestinationAddress.City, + State: ppmShipment.DestinationAddress.State, + PostalCode: ppmShipment.DestinationAddress.PostalCode, }, }, }, nil) @@ -140,9 +158,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "123 Third Street", - City: pickupAddress.City, - State: pickupAddress.State, - PostalCode: pickupAddress.PostalCode, + City: ppmShipment.PickupAddress.City, + State: ppmShipment.PickupAddress.State, + PostalCode: ppmShipment.PickupAddress.PostalCode, }, }, }, nil) @@ -150,9 +168,9 @@ func buildPPMShipmentWithBuildType(db *pop.Connection, customs []Customization, { Model: models.Address{ StreetAddress1: "1234 Third Street", - City: destinationAddress.City, - State: destinationAddress.State, - PostalCode: destinationAddress.PostalCode, + City: ppmShipment.DestinationAddress.City, + State: ppmShipment.DestinationAddress.State, + PostalCode: ppmShipment.DestinationAddress.PostalCode, }, }, }, nil) diff --git a/pkg/gen/ghcapi/configure_mymove.go b/pkg/gen/ghcapi/configure_mymove.go index 08d71eb7034..415ee1c890d 100644 --- a/pkg/gen/ghcapi/configure_mymove.go +++ b/pkg/gen/ghcapi/configure_mymove.go @@ -107,6 +107,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation shipment.ApproveShipmentDiversion has not yet been implemented") }) } + if api.ShipmentApproveShipmentsHandler == nil { + api.ShipmentApproveShipmentsHandler = shipment.ApproveShipmentsHandlerFunc(func(params shipment.ApproveShipmentsParams) middleware.Responder { + return middleware.NotImplemented("operation shipment.ApproveShipments has not yet been implemented") + }) + } if api.ReportViolationsAssociateReportViolationsHandler == nil { api.ReportViolationsAssociateReportViolationsHandler = report_violations.AssociateReportViolationsHandlerFunc(func(params report_violations.AssociateReportViolationsParams) middleware.Responder { return middleware.NotImplemented("operation report_violations.AssociateReportViolations has not yet been implemented") @@ -477,6 +482,11 @@ func configureAPI(api *ghcoperations.MymoveAPI) http.Handler { return middleware.NotImplemented("operation ppm.ShowAOAPacket has not yet been implemented") }) } + if api.TransportationOfficeShowCounselingOfficesHandler == nil { + api.TransportationOfficeShowCounselingOfficesHandler = transportation_office.ShowCounselingOfficesHandlerFunc(func(params transportation_office.ShowCounselingOfficesParams) middleware.Responder { + return middleware.NotImplemented("operation transportation_office.ShowCounselingOffices has not yet been implemented") + }) + } if api.PpmShowPaymentPacketHandler == nil { api.PpmShowPaymentPacketHandler = ppm.ShowPaymentPacketHandlerFunc(func(params ppm.ShowPaymentPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowPaymentPacket has not yet been implemented") diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go index 0704324506f..bb69f484f51 100644 --- a/pkg/gen/ghcapi/embedded_spec.go +++ b/pkg/gen/ghcapi/embedded_spec.go @@ -1516,7 +1516,7 @@ func init() { }, "/move-task-orders/{moveTaskOrderID}/status": { "patch": { - "description": "Changes move task order status to make it available to prime", + "description": "Changes move task order status", "consumes": [ "application/json" ], @@ -1526,7 +1526,7 @@ func init() { "tags": [ "moveTaskOrder" ], - "summary": "Change the status of a move task order to make it available to prime", + "summary": "Change the status of a move task order", "operationId": "updateMoveTaskOrderStatus", "parameters": [ { @@ -4617,6 +4617,12 @@ func init() { "description": "Used to illustrate which user is assigned to this payment request.\n", "name": "assignedTo", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -4970,6 +4976,12 @@ func init() { "description": "filters using a counselingOffice name of the move", "name": "counselingOffice", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -5124,6 +5136,12 @@ func init() { "description": "Used to return a queue for a GBLOC other than the default of the current user. Requires the HQ role or a secondary transportation office assignment. The parameter is ignored if the requesting user does not have the necessary role or assignment.\n", "name": "viewAsGBLOC", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -5395,6 +5413,64 @@ func init() { } ] }, + "/shipments/approve": { + "post": { + "description": "Approves multiple shipments in one request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "shipment" + ], + "summary": "Approves multiple shipments at once", + "operationId": "approveShipments", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApproveShipments" + } + } + ], + "responses": { + "200": { + "description": "Successfully approved the shipments", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/MTOShipment" + } + } + }, + "403": { + "$ref": "#/responses/PermissionDenied" + }, + "404": { + "$ref": "#/responses/NotFound" + }, + "409": { + "$ref": "#/responses/Conflict" + }, + "412": { + "$ref": "#/responses/PreconditionFailed" + }, + "422": { + "$ref": "#/responses/UnprocessableEntity" + }, + "500": { + "$ref": "#/responses/ServerError" + } + }, + "x-permissions": [ + "update.shipment" + ] + } + }, "/shipments/{shipmentID}": { "get": { "description": "fetches a shipment by ID", @@ -6402,7 +6478,7 @@ func init() { "operationId": "getTransportationOfficesGBLOCs", "responses": { "200": { - "description": "Successfully retrieved transportation offices", + "description": "Successfully retrieved GBLOCs", "schema": { "$ref": "#/definitions/GBLOCs" } @@ -6425,6 +6501,57 @@ func init() { } } }, + "/transportation_offices/{dutyLocationId}/counseling_offices/{serviceMemberId}": { + "get": { + "description": "Returns the counseling locations matching the GBLOC from the selected duty location", + "produces": [ + "application/json" + ], + "tags": [ + "transportationOffice" + ], + "summary": "Returns the counseling locations in the GBLOC matching the duty location", + "operationId": "showCounselingOffices", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "UUID of the duty location", + "name": "dutyLocationId", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "UUID of the service member, some counseling offices are branch specific", + "name": "serviceMemberId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved counseling offices", + "schema": { + "$ref": "#/definitions/CounselingOffices" + } + }, + "400": { + "$ref": "#/responses/InvalidRequest" + }, + "403": { + "$ref": "#/responses/PermissionDenied" + }, + "404": { + "$ref": "#/responses/NotFound" + }, + "500": { + "description": "internal server error" + } + } + } + }, "/uploads": { "post": { "description": "Uploads represent a single digital file, such as a JPEG or PDF. Currently, office application uploads are only for Services Counselors to upload files for orders, but this may be expanded in the future.", @@ -6896,6 +7023,33 @@ func init() { } } }, + "ApproveShipments": { + "type": "object", + "required": [ + "approveShipments" + ], + "properties": { + "approveShipments": { + "type": "array", + "items": { + "type": "object", + "required": [ + "shipmentID", + "eTag" + ], + "properties": { + "eTag": { + "type": "string" + }, + "shipmentID": { + "type": "string", + "format": "uuid" + } + } + } + } + } + }, "AssignOfficeUserBody": { "type": "object", "required": [ @@ -7142,6 +7296,30 @@ func init() { } } }, + "CounselingOffice": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "name": { + "type": "string", + "example": "Fort Bragg North Station" + } + } + }, + "CounselingOffices": { + "type": "array", + "items": { + "$ref": "#/definitions/CounselingOffice" + } + }, "CounselingUpdateAllowancePayload": { "type": "object", "properties": { @@ -7213,6 +7391,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 500 + }, + "weightRestriction": { + "description": "Indicates the weight restriction for a move to a particular location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -7748,6 +7932,12 @@ func init() { "x-nullable": true, "example": true }, + "counselingOfficeId": { + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "cf1addea-a4f9-4173-8506-2bb82a064cb7" + }, "departmentIndicator": { "$ref": "#/definitions/DeptIndicator" }, @@ -8420,6 +8610,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -10272,6 +10468,15 @@ func init() { "format": "uuid", "x-nullable": true }, + "counselingOffice": { + "$ref": "#/definitions/TransportationOffice" + }, + "counselingOfficeId": { + "description": "The transportation office that will handle services counseling for this move", + "type": "string", + "format": "uuid", + "x-nullable": true + }, "createdAt": { "type": "string", "format": "date-time" @@ -11500,6 +11705,27 @@ func init() { "readOnly": true, "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" }, + "intlLinehaulPrice": { + "description": "The full price of international shipping and linehaul (ISLH)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlPackPrice": { + "description": "The full price of international packing (IHPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlUnpackPrice": { + "description": "The full price of international unpacking (IHUPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, "miles": { "description": "The distance between the old address and the new address in miles.", "type": "integer", @@ -14286,6 +14512,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 500 + }, + "weightRestriction": { + "description": "Indicates the weight restriction for the move to a particular location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -17564,7 +17796,7 @@ func init() { }, "/move-task-orders/{moveTaskOrderID}/status": { "patch": { - "description": "Changes move task order status to make it available to prime", + "description": "Changes move task order status", "consumes": [ "application/json" ], @@ -17574,7 +17806,7 @@ func init() { "tags": [ "moveTaskOrder" ], - "summary": "Change the status of a move task order to make it available to prime", + "summary": "Change the status of a move task order", "operationId": "updateMoveTaskOrderStatus", "parameters": [ { @@ -21421,6 +21653,12 @@ func init() { "description": "Used to illustrate which user is assigned to this payment request.\n", "name": "assignedTo", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -21792,6 +22030,12 @@ func init() { "description": "filters using a counselingOffice name of the move", "name": "counselingOffice", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -21952,6 +22196,12 @@ func init() { "description": "Used to return a queue for a GBLOC other than the default of the current user. Requires the HQ role or a secondary transportation office assignment. The parameter is ignored if the requesting user does not have the necessary role or assignment.\n", "name": "viewAsGBLOC", "in": "query" + }, + { + "type": "string", + "description": "user's actively logged in role", + "name": "activeRole", + "in": "query" } ], "responses": { @@ -22298,6 +22548,82 @@ func init() { } ] }, + "/shipments/approve": { + "post": { + "description": "Approves multiple shipments in one request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "shipment" + ], + "summary": "Approves multiple shipments at once", + "operationId": "approveShipments", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApproveShipments" + } + } + ], + "responses": { + "200": { + "description": "Successfully approved the shipments", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/MTOShipment" + } + } + }, + "403": { + "description": "The request was denied", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The requested resource wasn't found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Conflict error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "412": { + "description": "Precondition failed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "The payload was unprocessable.", + "schema": { + "$ref": "#/definitions/ValidationError" + } + }, + "500": { + "description": "A server error occurred", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "x-permissions": [ + "update.shipment" + ] + } + }, "/shipments/{shipmentID}": { "get": { "description": "fetches a shipment by ID", @@ -23575,7 +23901,7 @@ func init() { "operationId": "getTransportationOfficesGBLOCs", "responses": { "200": { - "description": "Successfully retrieved transportation offices", + "description": "Successfully retrieved GBLOCs", "schema": { "$ref": "#/definitions/GBLOCs" } @@ -23613,6 +23939,66 @@ func init() { } } }, + "/transportation_offices/{dutyLocationId}/counseling_offices/{serviceMemberId}": { + "get": { + "description": "Returns the counseling locations matching the GBLOC from the selected duty location", + "produces": [ + "application/json" + ], + "tags": [ + "transportationOffice" + ], + "summary": "Returns the counseling locations in the GBLOC matching the duty location", + "operationId": "showCounselingOffices", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "UUID of the duty location", + "name": "dutyLocationId", + "in": "path", + "required": true + }, + { + "type": "string", + "format": "uuid", + "description": "UUID of the service member, some counseling offices are branch specific", + "name": "serviceMemberId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successfully retrieved counseling offices", + "schema": { + "$ref": "#/definitions/CounselingOffices" + } + }, + "400": { + "description": "The request payload is invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "The request was denied", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The requested resource wasn't found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "500": { + "description": "internal server error" + } + } + } + }, "/uploads": { "post": { "description": "Uploads represent a single digital file, such as a JPEG or PDF. Currently, office application uploads are only for Services Counselors to upload files for orders, but this may be expanded in the future.", @@ -24100,6 +24486,36 @@ func init() { } } }, + "ApproveShipments": { + "type": "object", + "required": [ + "approveShipments" + ], + "properties": { + "approveShipments": { + "type": "array", + "items": { + "$ref": "#/definitions/ApproveShipmentsApproveShipmentsItems0" + } + } + } + }, + "ApproveShipmentsApproveShipmentsItems0": { + "type": "object", + "required": [ + "shipmentID", + "eTag" + ], + "properties": { + "eTag": { + "type": "string" + }, + "shipmentID": { + "type": "string", + "format": "uuid" + } + } + }, "AssignOfficeUserBody": { "type": "object", "required": [ @@ -24346,6 +24762,30 @@ func init() { } } }, + "CounselingOffice": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "example": "c56a4180-65aa-42ec-a945-5fd21dec0538" + }, + "name": { + "type": "string", + "example": "Fort Bragg North Station" + } + } + }, + "CounselingOffices": { + "type": "array", + "items": { + "$ref": "#/definitions/CounselingOffice" + } + }, "CounselingUpdateAllowancePayload": { "type": "object", "properties": { @@ -24421,6 +24861,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 500 + }, + "weightRestriction": { + "description": "Indicates the weight restriction for a move to a particular location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -24956,6 +25402,12 @@ func init() { "x-nullable": true, "example": true }, + "counselingOfficeId": { + "type": "string", + "format": "uuid", + "x-nullable": true, + "example": "cf1addea-a4f9-4173-8506-2bb82a064cb7" + }, "departmentIndicator": { "$ref": "#/definitions/DeptIndicator" }, @@ -25628,6 +26080,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -27480,6 +27938,15 @@ func init() { "format": "uuid", "x-nullable": true }, + "counselingOffice": { + "$ref": "#/definitions/TransportationOffice" + }, + "counselingOfficeId": { + "description": "The transportation office that will handle services counseling for this move", + "type": "string", + "format": "uuid", + "x-nullable": true + }, "createdAt": { "type": "string", "format": "date-time" @@ -28708,6 +29175,27 @@ func init() { "readOnly": true, "example": "1f2270c7-7166-40ae-981e-b200ebdf3054" }, + "intlLinehaulPrice": { + "description": "The full price of international shipping and linehaul (ISLH)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlPackPrice": { + "description": "The full price of international packing (IHPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, + "intlUnpackPrice": { + "description": "The full price of international unpacking (IHUPK)", + "type": "integer", + "format": "cents", + "x-nullable": true, + "x-omitempty": false + }, "miles": { "description": "The distance between the old address and the new address in miles.", "type": "integer", @@ -31626,6 +32114,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 500 + }, + "weightRestriction": { + "description": "Indicates the weight restriction for the move to a particular location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, diff --git a/pkg/gen/ghcapi/ghcoperations/move_task_order/update_move_task_order_status.go b/pkg/gen/ghcapi/ghcoperations/move_task_order/update_move_task_order_status.go index ec8f17920aa..8b51f08902a 100644 --- a/pkg/gen/ghcapi/ghcoperations/move_task_order/update_move_task_order_status.go +++ b/pkg/gen/ghcapi/ghcoperations/move_task_order/update_move_task_order_status.go @@ -32,9 +32,9 @@ func NewUpdateMoveTaskOrderStatus(ctx *middleware.Context, handler UpdateMoveTas /* UpdateMoveTaskOrderStatus swagger:route PATCH /move-task-orders/{moveTaskOrderID}/status moveTaskOrder updateMoveTaskOrderStatus -# Change the status of a move task order to make it available to prime +# Change the status of a move task order -Changes move task order status to make it available to prime +Changes move task order status */ type UpdateMoveTaskOrderStatus struct { Context *middleware.Context diff --git a/pkg/gen/ghcapi/ghcoperations/mymove_api.go b/pkg/gen/ghcapi/ghcoperations/mymove_api.go index 61bda14b7d3..c7668464a5d 100644 --- a/pkg/gen/ghcapi/ghcoperations/mymove_api.go +++ b/pkg/gen/ghcapi/ghcoperations/mymove_api.go @@ -92,6 +92,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { ShipmentApproveShipmentDiversionHandler: shipment.ApproveShipmentDiversionHandlerFunc(func(params shipment.ApproveShipmentDiversionParams) middleware.Responder { return middleware.NotImplemented("operation shipment.ApproveShipmentDiversion has not yet been implemented") }), + ShipmentApproveShipmentsHandler: shipment.ApproveShipmentsHandlerFunc(func(params shipment.ApproveShipmentsParams) middleware.Responder { + return middleware.NotImplemented("operation shipment.ApproveShipments has not yet been implemented") + }), ReportViolationsAssociateReportViolationsHandler: report_violations.AssociateReportViolationsHandlerFunc(func(params report_violations.AssociateReportViolationsParams) middleware.Responder { return middleware.NotImplemented("operation report_violations.AssociateReportViolations has not yet been implemented") }), @@ -314,6 +317,9 @@ func NewMymoveAPI(spec *loads.Document) *MymoveAPI { PpmShowAOAPacketHandler: ppm.ShowAOAPacketHandlerFunc(func(params ppm.ShowAOAPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowAOAPacket has not yet been implemented") }), + TransportationOfficeShowCounselingOfficesHandler: transportation_office.ShowCounselingOfficesHandlerFunc(func(params transportation_office.ShowCounselingOfficesParams) middleware.Responder { + return middleware.NotImplemented("operation transportation_office.ShowCounselingOffices has not yet been implemented") + }), PpmShowPaymentPacketHandler: ppm.ShowPaymentPacketHandlerFunc(func(params ppm.ShowPaymentPacketParams) middleware.Responder { return middleware.NotImplemented("operation ppm.ShowPaymentPacket has not yet been implemented") }), @@ -458,6 +464,8 @@ type MymoveAPI struct { ShipmentApproveShipmentHandler shipment.ApproveShipmentHandler // ShipmentApproveShipmentDiversionHandler sets the operation handler for the approve shipment diversion operation ShipmentApproveShipmentDiversionHandler shipment.ApproveShipmentDiversionHandler + // ShipmentApproveShipmentsHandler sets the operation handler for the approve shipments operation + ShipmentApproveShipmentsHandler shipment.ApproveShipmentsHandler // ReportViolationsAssociateReportViolationsHandler sets the operation handler for the associate report violations operation ReportViolationsAssociateReportViolationsHandler report_violations.AssociateReportViolationsHandler // PaymentRequestsBulkDownloadHandler sets the operation handler for the bulk download operation @@ -606,6 +614,8 @@ type MymoveAPI struct { MoveSetFinancialReviewFlagHandler move.SetFinancialReviewFlagHandler // PpmShowAOAPacketHandler sets the operation handler for the show a o a packet operation PpmShowAOAPacketHandler ppm.ShowAOAPacketHandler + // TransportationOfficeShowCounselingOfficesHandler sets the operation handler for the show counseling offices operation + TransportationOfficeShowCounselingOfficesHandler transportation_office.ShowCounselingOfficesHandler // PpmShowPaymentPacketHandler sets the operation handler for the show payment packet operation PpmShowPaymentPacketHandler ppm.ShowPaymentPacketHandler // EvaluationReportsSubmitEvaluationReportHandler sets the operation handler for the submit evaluation report operation @@ -766,6 +776,9 @@ func (o *MymoveAPI) Validate() error { if o.ShipmentApproveShipmentDiversionHandler == nil { unregistered = append(unregistered, "shipment.ApproveShipmentDiversionHandler") } + if o.ShipmentApproveShipmentsHandler == nil { + unregistered = append(unregistered, "shipment.ApproveShipmentsHandler") + } if o.ReportViolationsAssociateReportViolationsHandler == nil { unregistered = append(unregistered, "report_violations.AssociateReportViolationsHandler") } @@ -988,6 +1001,9 @@ func (o *MymoveAPI) Validate() error { if o.PpmShowAOAPacketHandler == nil { unregistered = append(unregistered, "ppm.ShowAOAPacketHandler") } + if o.TransportationOfficeShowCounselingOfficesHandler == nil { + unregistered = append(unregistered, "transportation_office.ShowCounselingOfficesHandler") + } if o.PpmShowPaymentPacketHandler == nil { unregistered = append(unregistered, "ppm.ShowPaymentPacketHandler") } @@ -1195,6 +1211,10 @@ func (o *MymoveAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/shipments/approve"] = shipment.NewApproveShipments(o.context, o.ShipmentApproveShipmentsHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/report-violations/{reportID}"] = report_violations.NewAssociateReportViolations(o.context, o.ReportViolationsAssociateReportViolationsHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) @@ -1491,6 +1511,10 @@ func (o *MymoveAPI) initHandlerCache() { if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } + o.handlers["GET"]["/transportation_offices/{dutyLocationId}/counseling_offices/{serviceMemberId}"] = transportation_office.NewShowCounselingOffices(o.context, o.TransportationOfficeShowCounselingOfficesHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } o.handlers["GET"]["/ppm-shipments/{ppmShipmentId}/payment-packet"] = ppm.NewShowPaymentPacket(o.context, o.PpmShowPaymentPacketHandler) if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_parameters.go index f1cfe32cfae..2123cc801d9 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_parameters.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_parameters.go @@ -34,6 +34,10 @@ type GetMovesQueueParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /*user's actively logged in role + In: query + */ + ActiveRole *string /* In: query */ @@ -124,6 +128,11 @@ func (o *GetMovesQueueParams) BindRequest(r *http.Request, route *middleware.Mat qs := runtime.Values(r.URL.Query()) + qActiveRole, qhkActiveRole, _ := qs.GetOK("activeRole") + if err := o.bindActiveRole(qActiveRole, qhkActiveRole, route.Formats); err != nil { + res = append(res, err) + } + qAppearedInTooAt, qhkAppearedInTooAt, _ := qs.GetOK("appearedInTooAt") if err := o.bindAppearedInTooAt(qAppearedInTooAt, qhkAppearedInTooAt, route.Formats); err != nil { res = append(res, err) @@ -219,6 +228,24 @@ func (o *GetMovesQueueParams) BindRequest(r *http.Request, route *middleware.Mat return nil } +// bindActiveRole binds and validates parameter ActiveRole from query. +func (o *GetMovesQueueParams) bindActiveRole(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.ActiveRole = &raw + + return nil +} + // bindAppearedInTooAt binds and validates parameter AppearedInTooAt from query. func (o *GetMovesQueueParams) bindAppearedInTooAt(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_urlbuilder.go index ec05a50a3eb..7d809005df8 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_urlbuilder.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_moves_queue_urlbuilder.go @@ -16,6 +16,7 @@ import ( // GetMovesQueueURL generates an URL for the get moves queue operation type GetMovesQueueURL struct { + ActiveRole *string AppearedInTooAt *strfmt.DateTime AssignedTo *string Branch *string @@ -69,6 +70,14 @@ func (o *GetMovesQueueURL) Build() (*url.URL, error) { qs := make(url.Values) + var activeRoleQ string + if o.ActiveRole != nil { + activeRoleQ = *o.ActiveRole + } + if activeRoleQ != "" { + qs.Set("activeRole", activeRoleQ) + } + var appearedInTooAtQ string if o.AppearedInTooAt != nil { appearedInTooAtQ = o.AppearedInTooAt.String() diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go index fe0d201031e..423c3f7eaa6 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_parameters.go @@ -34,6 +34,10 @@ type GetPaymentRequestsQueueParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /*user's actively logged in role + In: query + */ + ActiveRole *string /*Used to illustrate which user is assigned to this payment request. In: query @@ -118,6 +122,11 @@ func (o *GetPaymentRequestsQueueParams) BindRequest(r *http.Request, route *midd qs := runtime.Values(r.URL.Query()) + qActiveRole, qhkActiveRole, _ := qs.GetOK("activeRole") + if err := o.bindActiveRole(qActiveRole, qhkActiveRole, route.Formats); err != nil { + res = append(res, err) + } + qAssignedTo, qhkAssignedTo, _ := qs.GetOK("assignedTo") if err := o.bindAssignedTo(qAssignedTo, qhkAssignedTo, route.Formats); err != nil { res = append(res, err) @@ -208,6 +217,24 @@ func (o *GetPaymentRequestsQueueParams) BindRequest(r *http.Request, route *midd return nil } +// bindActiveRole binds and validates parameter ActiveRole from query. +func (o *GetPaymentRequestsQueueParams) bindActiveRole(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.ActiveRole = &raw + + return nil +} + // bindAssignedTo binds and validates parameter AssignedTo from query. func (o *GetPaymentRequestsQueueParams) bindAssignedTo(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go index 1b5aa0e8b3b..45ac4629c59 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_payment_requests_queue_urlbuilder.go @@ -16,6 +16,7 @@ import ( // GetPaymentRequestsQueueURL generates an URL for the get payment requests queue operation type GetPaymentRequestsQueueURL struct { + ActiveRole *string AssignedTo *string Branch *string CounselingOffice *string @@ -68,6 +69,14 @@ func (o *GetPaymentRequestsQueueURL) Build() (*url.URL, error) { qs := make(url.Values) + var activeRoleQ string + if o.ActiveRole != nil { + activeRoleQ = *o.ActiveRole + } + if activeRoleQ != "" { + qs.Set("activeRole", activeRoleQ) + } + var assignedToQ string if o.AssignedTo != nil { assignedToQ = *o.AssignedTo diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_parameters.go b/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_parameters.go index 2b03f53918f..3f596c1f60c 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_parameters.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_parameters.go @@ -34,6 +34,10 @@ type GetServicesCounselingQueueParams struct { // HTTP Request Object HTTPRequest *http.Request `json:"-"` + /*user's actively logged in role + In: query + */ + ActiveRole *string /*Used to illustrate which user is assigned to this payment request. In: query @@ -148,6 +152,11 @@ func (o *GetServicesCounselingQueueParams) BindRequest(r *http.Request, route *m qs := runtime.Values(r.URL.Query()) + qActiveRole, qhkActiveRole, _ := qs.GetOK("activeRole") + if err := o.bindActiveRole(qActiveRole, qhkActiveRole, route.Formats); err != nil { + res = append(res, err) + } + qAssignedTo, qhkAssignedTo, _ := qs.GetOK("assignedTo") if err := o.bindAssignedTo(qAssignedTo, qhkAssignedTo, route.Formats); err != nil { res = append(res, err) @@ -273,6 +282,24 @@ func (o *GetServicesCounselingQueueParams) BindRequest(r *http.Request, route *m return nil } +// bindActiveRole binds and validates parameter ActiveRole from query. +func (o *GetServicesCounselingQueueParams) bindActiveRole(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + o.ActiveRole = &raw + + return nil +} + // bindAssignedTo binds and validates parameter AssignedTo from query. func (o *GetServicesCounselingQueueParams) bindAssignedTo(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_urlbuilder.go index d7ad7668c07..b013d4b0089 100644 --- a/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_urlbuilder.go +++ b/pkg/gen/ghcapi/ghcoperations/queues/get_services_counseling_queue_urlbuilder.go @@ -16,6 +16,7 @@ import ( // GetServicesCounselingQueueURL generates an URL for the get services counseling queue operation type GetServicesCounselingQueueURL struct { + ActiveRole *string AssignedTo *string Branch *string CloseoutInitiated *strfmt.DateTime @@ -75,6 +76,14 @@ func (o *GetServicesCounselingQueueURL) Build() (*url.URL, error) { qs := make(url.Values) + var activeRoleQ string + if o.ActiveRole != nil { + activeRoleQ = *o.ActiveRole + } + if activeRoleQ != "" { + qs.Set("activeRole", activeRoleQ) + } + var assignedToQ string if o.AssignedTo != nil { assignedToQ = *o.AssignedTo diff --git a/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments.go b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments.go new file mode 100644 index 00000000000..c441f320f1a --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package shipment + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// ApproveShipmentsHandlerFunc turns a function with the right signature into a approve shipments handler +type ApproveShipmentsHandlerFunc func(ApproveShipmentsParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn ApproveShipmentsHandlerFunc) Handle(params ApproveShipmentsParams) middleware.Responder { + return fn(params) +} + +// ApproveShipmentsHandler interface for that can handle valid approve shipments params +type ApproveShipmentsHandler interface { + Handle(ApproveShipmentsParams) middleware.Responder +} + +// NewApproveShipments creates a new http.Handler for the approve shipments operation +func NewApproveShipments(ctx *middleware.Context, handler ApproveShipmentsHandler) *ApproveShipments { + return &ApproveShipments{Context: ctx, Handler: handler} +} + +/* + ApproveShipments swagger:route POST /shipments/approve shipment approveShipments + +# Approves multiple shipments at once + +Approves multiple shipments in one request +*/ +type ApproveShipments struct { + Context *middleware.Context + Handler ApproveShipmentsHandler +} + +func (o *ApproveShipments) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewApproveShipmentsParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_parameters.go b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_parameters.go new file mode 100644 index 00000000000..2c11422365c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_parameters.go @@ -0,0 +1,84 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package shipment + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/validate" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// NewApproveShipmentsParams creates a new ApproveShipmentsParams object +// +// There are no default values defined in the spec. +func NewApproveShipmentsParams() ApproveShipmentsParams { + + return ApproveShipmentsParams{} +} + +// ApproveShipmentsParams contains all the bound params for the approve shipments operation +// typically these are obtained from a http.Request +// +// swagger:parameters approveShipments +type ApproveShipmentsParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Body *ghcmessages.ApproveShipments +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewApproveShipmentsParams() beforehand. +func (o *ApproveShipmentsParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body ghcmessages.ApproveShipments + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("body", "body", "")) + } else { + res = append(res, errors.NewParseError("body", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + ctx := validate.WithOperationRequest(r.Context()) + if err := body.ContextValidate(ctx, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Body = &body + } + } + } else { + res = append(res, errors.Required("body", "body", "")) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_responses.go b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_responses.go new file mode 100644 index 00000000000..44dd124ba3c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_responses.go @@ -0,0 +1,332 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package shipment + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// ApproveShipmentsOKCode is the HTTP code returned for type ApproveShipmentsOK +const ApproveShipmentsOKCode int = 200 + +/* +ApproveShipmentsOK Successfully approved the shipments + +swagger:response approveShipmentsOK +*/ +type ApproveShipmentsOK struct { + + /* + In: Body + */ + Payload []*ghcmessages.MTOShipment `json:"body,omitempty"` +} + +// NewApproveShipmentsOK creates ApproveShipmentsOK with default headers values +func NewApproveShipmentsOK() *ApproveShipmentsOK { + + return &ApproveShipmentsOK{} +} + +// WithPayload adds the payload to the approve shipments o k response +func (o *ApproveShipmentsOK) WithPayload(payload []*ghcmessages.MTOShipment) *ApproveShipmentsOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments o k response +func (o *ApproveShipmentsOK) SetPayload(payload []*ghcmessages.MTOShipment) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if payload == nil { + // return empty array + payload = make([]*ghcmessages.MTOShipment, 0, 50) + } + + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ApproveShipmentsForbiddenCode is the HTTP code returned for type ApproveShipmentsForbidden +const ApproveShipmentsForbiddenCode int = 403 + +/* +ApproveShipmentsForbidden The request was denied + +swagger:response approveShipmentsForbidden +*/ +type ApproveShipmentsForbidden struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewApproveShipmentsForbidden creates ApproveShipmentsForbidden with default headers values +func NewApproveShipmentsForbidden() *ApproveShipmentsForbidden { + + return &ApproveShipmentsForbidden{} +} + +// WithPayload adds the payload to the approve shipments forbidden response +func (o *ApproveShipmentsForbidden) WithPayload(payload *ghcmessages.Error) *ApproveShipmentsForbidden { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments forbidden response +func (o *ApproveShipmentsForbidden) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsForbidden) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(403) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ApproveShipmentsNotFoundCode is the HTTP code returned for type ApproveShipmentsNotFound +const ApproveShipmentsNotFoundCode int = 404 + +/* +ApproveShipmentsNotFound The requested resource wasn't found + +swagger:response approveShipmentsNotFound +*/ +type ApproveShipmentsNotFound struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewApproveShipmentsNotFound creates ApproveShipmentsNotFound with default headers values +func NewApproveShipmentsNotFound() *ApproveShipmentsNotFound { + + return &ApproveShipmentsNotFound{} +} + +// WithPayload adds the payload to the approve shipments not found response +func (o *ApproveShipmentsNotFound) WithPayload(payload *ghcmessages.Error) *ApproveShipmentsNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments not found response +func (o *ApproveShipmentsNotFound) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ApproveShipmentsConflictCode is the HTTP code returned for type ApproveShipmentsConflict +const ApproveShipmentsConflictCode int = 409 + +/* +ApproveShipmentsConflict Conflict error + +swagger:response approveShipmentsConflict +*/ +type ApproveShipmentsConflict struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewApproveShipmentsConflict creates ApproveShipmentsConflict with default headers values +func NewApproveShipmentsConflict() *ApproveShipmentsConflict { + + return &ApproveShipmentsConflict{} +} + +// WithPayload adds the payload to the approve shipments conflict response +func (o *ApproveShipmentsConflict) WithPayload(payload *ghcmessages.Error) *ApproveShipmentsConflict { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments conflict response +func (o *ApproveShipmentsConflict) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsConflict) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(409) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ApproveShipmentsPreconditionFailedCode is the HTTP code returned for type ApproveShipmentsPreconditionFailed +const ApproveShipmentsPreconditionFailedCode int = 412 + +/* +ApproveShipmentsPreconditionFailed Precondition failed + +swagger:response approveShipmentsPreconditionFailed +*/ +type ApproveShipmentsPreconditionFailed struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewApproveShipmentsPreconditionFailed creates ApproveShipmentsPreconditionFailed with default headers values +func NewApproveShipmentsPreconditionFailed() *ApproveShipmentsPreconditionFailed { + + return &ApproveShipmentsPreconditionFailed{} +} + +// WithPayload adds the payload to the approve shipments precondition failed response +func (o *ApproveShipmentsPreconditionFailed) WithPayload(payload *ghcmessages.Error) *ApproveShipmentsPreconditionFailed { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments precondition failed response +func (o *ApproveShipmentsPreconditionFailed) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsPreconditionFailed) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(412) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ApproveShipmentsUnprocessableEntityCode is the HTTP code returned for type ApproveShipmentsUnprocessableEntity +const ApproveShipmentsUnprocessableEntityCode int = 422 + +/* +ApproveShipmentsUnprocessableEntity The payload was unprocessable. + +swagger:response approveShipmentsUnprocessableEntity +*/ +type ApproveShipmentsUnprocessableEntity struct { + + /* + In: Body + */ + Payload *ghcmessages.ValidationError `json:"body,omitempty"` +} + +// NewApproveShipmentsUnprocessableEntity creates ApproveShipmentsUnprocessableEntity with default headers values +func NewApproveShipmentsUnprocessableEntity() *ApproveShipmentsUnprocessableEntity { + + return &ApproveShipmentsUnprocessableEntity{} +} + +// WithPayload adds the payload to the approve shipments unprocessable entity response +func (o *ApproveShipmentsUnprocessableEntity) WithPayload(payload *ghcmessages.ValidationError) *ApproveShipmentsUnprocessableEntity { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments unprocessable entity response +func (o *ApproveShipmentsUnprocessableEntity) SetPayload(payload *ghcmessages.ValidationError) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsUnprocessableEntity) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(422) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ApproveShipmentsInternalServerErrorCode is the HTTP code returned for type ApproveShipmentsInternalServerError +const ApproveShipmentsInternalServerErrorCode int = 500 + +/* +ApproveShipmentsInternalServerError A server error occurred + +swagger:response approveShipmentsInternalServerError +*/ +type ApproveShipmentsInternalServerError struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewApproveShipmentsInternalServerError creates ApproveShipmentsInternalServerError with default headers values +func NewApproveShipmentsInternalServerError() *ApproveShipmentsInternalServerError { + + return &ApproveShipmentsInternalServerError{} +} + +// WithPayload adds the payload to the approve shipments internal server error response +func (o *ApproveShipmentsInternalServerError) WithPayload(payload *ghcmessages.Error) *ApproveShipmentsInternalServerError { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the approve shipments internal server error response +func (o *ApproveShipmentsInternalServerError) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ApproveShipmentsInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(500) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_urlbuilder.go new file mode 100644 index 00000000000..de99aa28fa3 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/shipment/approve_shipments_urlbuilder.go @@ -0,0 +1,87 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package shipment + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// ApproveShipmentsURL generates an URL for the approve shipments operation +type ApproveShipmentsURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ApproveShipmentsURL) WithBasePath(bp string) *ApproveShipmentsURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ApproveShipmentsURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *ApproveShipmentsURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/shipments/approve" + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *ApproveShipmentsURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *ApproveShipmentsURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *ApproveShipmentsURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on ApproveShipmentsURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on ApproveShipmentsURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *ApproveShipmentsURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go index 309de84d0fa..c630be03fd6 100644 --- a/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/get_transportation_offices_g_b_l_o_cs_responses.go @@ -17,7 +17,7 @@ import ( const GetTransportationOfficesGBLOCsOKCode int = 200 /* -GetTransportationOfficesGBLOCsOK Successfully retrieved transportation offices +GetTransportationOfficesGBLOCsOK Successfully retrieved GBLOCs swagger:response getTransportationOfficesGBLOCsOK */ diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go new file mode 100644 index 00000000000..b0cb58981df --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices.go @@ -0,0 +1,58 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// ShowCounselingOfficesHandlerFunc turns a function with the right signature into a show counseling offices handler +type ShowCounselingOfficesHandlerFunc func(ShowCounselingOfficesParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn ShowCounselingOfficesHandlerFunc) Handle(params ShowCounselingOfficesParams) middleware.Responder { + return fn(params) +} + +// ShowCounselingOfficesHandler interface for that can handle valid show counseling offices params +type ShowCounselingOfficesHandler interface { + Handle(ShowCounselingOfficesParams) middleware.Responder +} + +// NewShowCounselingOffices creates a new http.Handler for the show counseling offices operation +func NewShowCounselingOffices(ctx *middleware.Context, handler ShowCounselingOfficesHandler) *ShowCounselingOffices { + return &ShowCounselingOffices{Context: ctx, Handler: handler} +} + +/* + ShowCounselingOffices swagger:route GET /transportation_offices/{dutyLocationId}/counseling_offices/{serviceMemberId} transportationOffice showCounselingOffices + +# Returns the counseling locations in the GBLOC matching the duty location + +Returns the counseling locations matching the GBLOC from the selected duty location +*/ +type ShowCounselingOffices struct { + Context *middleware.Context + Handler ShowCounselingOfficesHandler +} + +func (o *ShowCounselingOffices) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewShowCounselingOfficesParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go new file mode 100644 index 00000000000..91d0a12238c --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_parameters.go @@ -0,0 +1,134 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// NewShowCounselingOfficesParams creates a new ShowCounselingOfficesParams object +// +// There are no default values defined in the spec. +func NewShowCounselingOfficesParams() ShowCounselingOfficesParams { + + return ShowCounselingOfficesParams{} +} + +// ShowCounselingOfficesParams contains all the bound params for the show counseling offices operation +// typically these are obtained from a http.Request +// +// swagger:parameters showCounselingOffices +type ShowCounselingOfficesParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*UUID of the duty location + Required: true + In: path + */ + DutyLocationID strfmt.UUID + /*UUID of the service member, some counseling offices are branch specific + Required: true + In: path + */ + ServiceMemberID strfmt.UUID +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewShowCounselingOfficesParams() beforehand. +func (o *ShowCounselingOfficesParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rDutyLocationID, rhkDutyLocationID, _ := route.Params.GetOK("dutyLocationId") + if err := o.bindDutyLocationID(rDutyLocationID, rhkDutyLocationID, route.Formats); err != nil { + res = append(res, err) + } + + rServiceMemberID, rhkServiceMemberID, _ := route.Params.GetOK("serviceMemberId") + if err := o.bindServiceMemberID(rServiceMemberID, rhkServiceMemberID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindDutyLocationID binds and validates parameter DutyLocationID from path. +func (o *ShowCounselingOfficesParams) bindDutyLocationID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + // Format: uuid + value, err := formats.Parse("uuid", raw) + if err != nil { + return errors.InvalidType("dutyLocationId", "path", "strfmt.UUID", raw) + } + o.DutyLocationID = *(value.(*strfmt.UUID)) + + if err := o.validateDutyLocationID(formats); err != nil { + return err + } + + return nil +} + +// validateDutyLocationID carries on validations for parameter DutyLocationID +func (o *ShowCounselingOfficesParams) validateDutyLocationID(formats strfmt.Registry) error { + + if err := validate.FormatOf("dutyLocationId", "path", "uuid", o.DutyLocationID.String(), formats); err != nil { + return err + } + return nil +} + +// bindServiceMemberID binds and validates parameter ServiceMemberID from path. +func (o *ShowCounselingOfficesParams) bindServiceMemberID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + // Format: uuid + value, err := formats.Parse("uuid", raw) + if err != nil { + return errors.InvalidType("serviceMemberId", "path", "strfmt.UUID", raw) + } + o.ServiceMemberID = *(value.(*strfmt.UUID)) + + if err := o.validateServiceMemberID(formats); err != nil { + return err + } + + return nil +} + +// validateServiceMemberID carries on validations for parameter ServiceMemberID +func (o *ShowCounselingOfficesParams) validateServiceMemberID(formats strfmt.Registry) error { + + if err := validate.FormatOf("serviceMemberId", "path", "uuid", o.ServiceMemberID.String(), formats); err != nil { + return err + } + return nil +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go new file mode 100644 index 00000000000..0e38c7ca905 --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_responses.go @@ -0,0 +1,222 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/transcom/mymove/pkg/gen/ghcmessages" +) + +// ShowCounselingOfficesOKCode is the HTTP code returned for type ShowCounselingOfficesOK +const ShowCounselingOfficesOKCode int = 200 + +/* +ShowCounselingOfficesOK Successfully retrieved counseling offices + +swagger:response showCounselingOfficesOK +*/ +type ShowCounselingOfficesOK struct { + + /* + In: Body + */ + Payload ghcmessages.CounselingOffices `json:"body,omitempty"` +} + +// NewShowCounselingOfficesOK creates ShowCounselingOfficesOK with default headers values +func NewShowCounselingOfficesOK() *ShowCounselingOfficesOK { + + return &ShowCounselingOfficesOK{} +} + +// WithPayload adds the payload to the show counseling offices o k response +func (o *ShowCounselingOfficesOK) WithPayload(payload ghcmessages.CounselingOffices) *ShowCounselingOfficesOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the show counseling offices o k response +func (o *ShowCounselingOfficesOK) SetPayload(payload ghcmessages.CounselingOffices) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if payload == nil { + // return empty array + payload = ghcmessages.CounselingOffices{} + } + + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// ShowCounselingOfficesBadRequestCode is the HTTP code returned for type ShowCounselingOfficesBadRequest +const ShowCounselingOfficesBadRequestCode int = 400 + +/* +ShowCounselingOfficesBadRequest The request payload is invalid + +swagger:response showCounselingOfficesBadRequest +*/ +type ShowCounselingOfficesBadRequest struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewShowCounselingOfficesBadRequest creates ShowCounselingOfficesBadRequest with default headers values +func NewShowCounselingOfficesBadRequest() *ShowCounselingOfficesBadRequest { + + return &ShowCounselingOfficesBadRequest{} +} + +// WithPayload adds the payload to the show counseling offices bad request response +func (o *ShowCounselingOfficesBadRequest) WithPayload(payload *ghcmessages.Error) *ShowCounselingOfficesBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the show counseling offices bad request response +func (o *ShowCounselingOfficesBadRequest) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ShowCounselingOfficesForbiddenCode is the HTTP code returned for type ShowCounselingOfficesForbidden +const ShowCounselingOfficesForbiddenCode int = 403 + +/* +ShowCounselingOfficesForbidden The request was denied + +swagger:response showCounselingOfficesForbidden +*/ +type ShowCounselingOfficesForbidden struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewShowCounselingOfficesForbidden creates ShowCounselingOfficesForbidden with default headers values +func NewShowCounselingOfficesForbidden() *ShowCounselingOfficesForbidden { + + return &ShowCounselingOfficesForbidden{} +} + +// WithPayload adds the payload to the show counseling offices forbidden response +func (o *ShowCounselingOfficesForbidden) WithPayload(payload *ghcmessages.Error) *ShowCounselingOfficesForbidden { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the show counseling offices forbidden response +func (o *ShowCounselingOfficesForbidden) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesForbidden) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(403) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ShowCounselingOfficesNotFoundCode is the HTTP code returned for type ShowCounselingOfficesNotFound +const ShowCounselingOfficesNotFoundCode int = 404 + +/* +ShowCounselingOfficesNotFound The requested resource wasn't found + +swagger:response showCounselingOfficesNotFound +*/ +type ShowCounselingOfficesNotFound struct { + + /* + In: Body + */ + Payload *ghcmessages.Error `json:"body,omitempty"` +} + +// NewShowCounselingOfficesNotFound creates ShowCounselingOfficesNotFound with default headers values +func NewShowCounselingOfficesNotFound() *ShowCounselingOfficesNotFound { + + return &ShowCounselingOfficesNotFound{} +} + +// WithPayload adds the payload to the show counseling offices not found response +func (o *ShowCounselingOfficesNotFound) WithPayload(payload *ghcmessages.Error) *ShowCounselingOfficesNotFound { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the show counseling offices not found response +func (o *ShowCounselingOfficesNotFound) SetPayload(payload *ghcmessages.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(404) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ShowCounselingOfficesInternalServerErrorCode is the HTTP code returned for type ShowCounselingOfficesInternalServerError +const ShowCounselingOfficesInternalServerErrorCode int = 500 + +/* +ShowCounselingOfficesInternalServerError internal server error + +swagger:response showCounselingOfficesInternalServerError +*/ +type ShowCounselingOfficesInternalServerError struct { +} + +// NewShowCounselingOfficesInternalServerError creates ShowCounselingOfficesInternalServerError with default headers values +func NewShowCounselingOfficesInternalServerError() *ShowCounselingOfficesInternalServerError { + + return &ShowCounselingOfficesInternalServerError{} +} + +// WriteResponse to the client +func (o *ShowCounselingOfficesInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(500) +} diff --git a/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go new file mode 100644 index 00000000000..269dfc8874e --- /dev/null +++ b/pkg/gen/ghcapi/ghcoperations/transportation_office/show_counseling_offices_urlbuilder.go @@ -0,0 +1,109 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package transportation_office + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" + + "github.com/go-openapi/strfmt" +) + +// ShowCounselingOfficesURL generates an URL for the show counseling offices operation +type ShowCounselingOfficesURL struct { + DutyLocationID strfmt.UUID + ServiceMemberID strfmt.UUID + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ShowCounselingOfficesURL) WithBasePath(bp string) *ShowCounselingOfficesURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *ShowCounselingOfficesURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *ShowCounselingOfficesURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/transportation_offices/{dutyLocationId}/counseling_offices/{serviceMemberId}" + + dutyLocationID := o.DutyLocationID.String() + if dutyLocationID != "" { + _path = strings.Replace(_path, "{dutyLocationId}", dutyLocationID, -1) + } else { + return nil, errors.New("dutyLocationId is required on ShowCounselingOfficesURL") + } + + serviceMemberID := o.ServiceMemberID.String() + if serviceMemberID != "" { + _path = strings.Replace(_path, "{serviceMemberId}", serviceMemberID, -1) + } else { + return nil, errors.New("serviceMemberId is required on ShowCounselingOfficesURL") + } + + _basePath := o._basePath + if _basePath == "" { + _basePath = "/ghc/v1" + } + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *ShowCounselingOfficesURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *ShowCounselingOfficesURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *ShowCounselingOfficesURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on ShowCounselingOfficesURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on ShowCounselingOfficesURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *ShowCounselingOfficesURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/gen/ghcmessages/approve_shipments.go b/pkg/gen/ghcmessages/approve_shipments.go new file mode 100644 index 00000000000..c86a5252416 --- /dev/null +++ b/pkg/gen/ghcmessages/approve_shipments.go @@ -0,0 +1,202 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ApproveShipments approve shipments +// +// swagger:model ApproveShipments +type ApproveShipments struct { + + // approve shipments + // Required: true + ApproveShipments []*ApproveShipmentsApproveShipmentsItems0 `json:"approveShipments"` +} + +// Validate validates this approve shipments +func (m *ApproveShipments) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateApproveShipments(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ApproveShipments) validateApproveShipments(formats strfmt.Registry) error { + + if err := validate.Required("approveShipments", "body", m.ApproveShipments); err != nil { + return err + } + + for i := 0; i < len(m.ApproveShipments); i++ { + if swag.IsZero(m.ApproveShipments[i]) { // not required + continue + } + + if m.ApproveShipments[i] != nil { + if err := m.ApproveShipments[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("approveShipments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("approveShipments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this approve shipments based on the context it is used +func (m *ApproveShipments) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateApproveShipments(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ApproveShipments) contextValidateApproveShipments(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.ApproveShipments); i++ { + + if m.ApproveShipments[i] != nil { + + if swag.IsZero(m.ApproveShipments[i]) { // not required + return nil + } + + if err := m.ApproveShipments[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("approveShipments" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("approveShipments" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ApproveShipments) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ApproveShipments) UnmarshalBinary(b []byte) error { + var res ApproveShipments + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// ApproveShipmentsApproveShipmentsItems0 approve shipments approve shipments items0 +// +// swagger:model ApproveShipmentsApproveShipmentsItems0 +type ApproveShipmentsApproveShipmentsItems0 struct { + + // e tag + // Required: true + ETag *string `json:"eTag"` + + // shipment ID + // Required: true + // Format: uuid + ShipmentID *strfmt.UUID `json:"shipmentID"` +} + +// Validate validates this approve shipments approve shipments items0 +func (m *ApproveShipmentsApproveShipmentsItems0) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateETag(formats); err != nil { + res = append(res, err) + } + + if err := m.validateShipmentID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ApproveShipmentsApproveShipmentsItems0) validateETag(formats strfmt.Registry) error { + + if err := validate.Required("eTag", "body", m.ETag); err != nil { + return err + } + + return nil +} + +func (m *ApproveShipmentsApproveShipmentsItems0) validateShipmentID(formats strfmt.Registry) error { + + if err := validate.Required("shipmentID", "body", m.ShipmentID); err != nil { + return err + } + + if err := validate.FormatOf("shipmentID", "body", "uuid", m.ShipmentID.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this approve shipments approve shipments items0 based on context it is used +func (m *ApproveShipmentsApproveShipmentsItems0) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ApproveShipmentsApproveShipmentsItems0) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ApproveShipmentsApproveShipmentsItems0) UnmarshalBinary(b []byte) error { + var res ApproveShipmentsApproveShipmentsItems0 + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/counseling_office.go b/pkg/gen/ghcmessages/counseling_office.go new file mode 100644 index 00000000000..b3bf2fea949 --- /dev/null +++ b/pkg/gen/ghcmessages/counseling_office.go @@ -0,0 +1,95 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// CounselingOffice counseling office +// +// swagger:model CounselingOffice +type CounselingOffice struct { + + // id + // Example: c56a4180-65aa-42ec-a945-5fd21dec0538 + // Required: true + // Format: uuid + ID *strfmt.UUID `json:"id"` + + // name + // Example: Fort Bragg North Station + // Required: true + Name *string `json:"name"` +} + +// Validate validates this counseling office +func (m *CounselingOffice) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateName(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *CounselingOffice) validateID(formats strfmt.Registry) error { + + if err := validate.Required("id", "body", m.ID); err != nil { + return err + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *CounselingOffice) validateName(formats strfmt.Registry) error { + + if err := validate.Required("name", "body", m.Name); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this counseling office based on context it is used +func (m *CounselingOffice) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *CounselingOffice) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *CounselingOffice) UnmarshalBinary(b []byte) error { + var res CounselingOffice + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/ghcmessages/counseling_offices.go b/pkg/gen/ghcmessages/counseling_offices.go new file mode 100644 index 00000000000..28a6d79e3b3 --- /dev/null +++ b/pkg/gen/ghcmessages/counseling_offices.go @@ -0,0 +1,78 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package ghcmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// CounselingOffices counseling offices +// +// swagger:model CounselingOffices +type CounselingOffices []*CounselingOffice + +// Validate validates this counseling offices +func (m CounselingOffices) Validate(formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + if swag.IsZero(m[i]) { // not required + continue + } + + if m[i] != nil { + if err := m[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// ContextValidate validate this counseling offices based on the context it is used +func (m CounselingOffices) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + for i := 0; i < len(m); i++ { + + if m[i] != nil { + + if swag.IsZero(m[i]) { // not required + return nil + } + + if err := m[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName(strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName(strconv.Itoa(i)) + } + return err + } + } + + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/gen/ghcmessages/counseling_update_allowance_payload.go b/pkg/gen/ghcmessages/counseling_update_allowance_payload.go index d6bed9fac0c..805a206b000 100644 --- a/pkg/gen/ghcmessages/counseling_update_allowance_payload.go +++ b/pkg/gen/ghcmessages/counseling_update_allowance_payload.go @@ -70,6 +70,10 @@ type CounselingUpdateAllowancePayload struct { // ub allowance // Example: 500 UbAllowance *int64 `json:"ubAllowance,omitempty"` + + // Indicates the weight restriction for a move to a particular location. + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this counseling update allowance payload diff --git a/pkg/gen/ghcmessages/create_orders.go b/pkg/gen/ghcmessages/create_orders.go index 25f77f42c15..a71a85a4d74 100644 --- a/pkg/gen/ghcmessages/create_orders.go +++ b/pkg/gen/ghcmessages/create_orders.go @@ -23,6 +23,11 @@ type CreateOrders struct { // Example: true AccompaniedTour *bool `json:"accompaniedTour,omitempty"` + // counseling office Id + // Example: cf1addea-a4f9-4173-8506-2bb82a064cb7 + // Format: uuid + CounselingOfficeID *strfmt.UUID `json:"counselingOfficeId,omitempty"` + // department indicator DepartmentIndicator *DeptIndicator `json:"departmentIndicator,omitempty"` @@ -100,6 +105,10 @@ type CreateOrders struct { func (m *CreateOrders) Validate(formats strfmt.Registry) error { var res []error + if err := m.validateCounselingOfficeID(formats); err != nil { + res = append(res, err) + } + if err := m.validateDepartmentIndicator(formats); err != nil { res = append(res, err) } @@ -150,6 +159,18 @@ func (m *CreateOrders) Validate(formats strfmt.Registry) error { return nil } +func (m *CreateOrders) validateCounselingOfficeID(formats strfmt.Registry) error { + if swag.IsZero(m.CounselingOfficeID) { // not required + return nil + } + + if err := validate.FormatOf("counselingOfficeId", "body", "uuid", m.CounselingOfficeID.String(), formats); err != nil { + return err + } + + return nil +} + func (m *CreateOrders) validateDepartmentIndicator(formats strfmt.Registry) error { if swag.IsZero(m.DepartmentIndicator) { // not required return nil diff --git a/pkg/gen/ghcmessages/entitlements.go b/pkg/gen/ghcmessages/entitlements.go index 2ee15f3d03a..e856534cc33 100644 --- a/pkg/gen/ghcmessages/entitlements.go +++ b/pkg/gen/ghcmessages/entitlements.go @@ -90,6 +90,10 @@ type Entitlements struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UnaccompaniedBaggageAllowance *int64 `json:"unaccompaniedBaggageAllowance,omitempty"` + + // weight restriction + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this entitlements diff --git a/pkg/gen/ghcmessages/move.go b/pkg/gen/ghcmessages/move.go index 26d652a3f42..604dff87fb7 100644 --- a/pkg/gen/ghcmessages/move.go +++ b/pkg/gen/ghcmessages/move.go @@ -61,6 +61,13 @@ type Move struct { // Format: uuid ContractorID *strfmt.UUID `json:"contractorId,omitempty"` + // counseling office + CounselingOffice *TransportationOffice `json:"counselingOffice,omitempty"` + + // The transportation office that will handle services counseling for this move + // Format: uuid + CounselingOfficeID *strfmt.UUID `json:"counselingOfficeId,omitempty"` + // created at // Format: date-time CreatedAt strfmt.DateTime `json:"createdAt,omitempty"` @@ -201,6 +208,14 @@ func (m *Move) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateCounselingOffice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateCounselingOfficeID(formats); err != nil { + res = append(res, err) + } + if err := m.validateCreatedAt(formats); err != nil { res = append(res, err) } @@ -457,6 +472,37 @@ func (m *Move) validateContractorID(formats strfmt.Registry) error { return nil } +func (m *Move) validateCounselingOffice(formats strfmt.Registry) error { + if swag.IsZero(m.CounselingOffice) { // not required + return nil + } + + if m.CounselingOffice != nil { + if err := m.CounselingOffice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("counselingOffice") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("counselingOffice") + } + return err + } + } + + return nil +} + +func (m *Move) validateCounselingOfficeID(formats strfmt.Registry) error { + if swag.IsZero(m.CounselingOfficeID) { // not required + return nil + } + + if err := validate.FormatOf("counselingOfficeId", "body", "uuid", m.CounselingOfficeID.String(), formats); err != nil { + return err + } + + return nil +} + func (m *Move) validateCreatedAt(formats strfmt.Registry) error { if swag.IsZero(m.CreatedAt) { // not required return nil @@ -701,6 +747,10 @@ func (m *Move) ContextValidate(ctx context.Context, formats strfmt.Registry) err res = append(res, err) } + if err := m.contextValidateCounselingOffice(ctx, formats); err != nil { + res = append(res, err) + } + if err := m.contextValidateFinancialReviewFlag(ctx, formats); err != nil { res = append(res, err) } @@ -857,6 +907,27 @@ func (m *Move) contextValidateContractor(ctx context.Context, formats strfmt.Reg return nil } +func (m *Move) contextValidateCounselingOffice(ctx context.Context, formats strfmt.Registry) error { + + if m.CounselingOffice != nil { + + if swag.IsZero(m.CounselingOffice) { // not required + return nil + } + + if err := m.CounselingOffice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("counselingOffice") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("counselingOffice") + } + return err + } + } + + return nil +} + func (m *Move) contextValidateFinancialReviewFlag(ctx context.Context, formats strfmt.Registry) error { if err := validate.ReadOnly(ctx, "financialReviewFlag", "body", bool(m.FinancialReviewFlag)); err != nil { diff --git a/pkg/gen/ghcmessages/p_p_m_closeout.go b/pkg/gen/ghcmessages/p_p_m_closeout.go index b0f423ba61a..a84e0e4c2e0 100644 --- a/pkg/gen/ghcmessages/p_p_m_closeout.go +++ b/pkg/gen/ghcmessages/p_p_m_closeout.go @@ -69,6 +69,15 @@ type PPMCloseout struct { // Format: uuid ID strfmt.UUID `json:"id"` + // The full price of international shipping and linehaul (ISLH) + IntlLinehaulPrice *int64 `json:"intlLinehaulPrice"` + + // The full price of international packing (IHPK) + IntlPackPrice *int64 `json:"intlPackPrice"` + + // The full price of international unpacking (IHUPK) + IntlUnpackPrice *int64 `json:"intlUnpackPrice"` + // The distance between the old address and the new address in miles. // Example: 54 // Minimum: 0 diff --git a/pkg/gen/ghcmessages/update_allowance_payload.go b/pkg/gen/ghcmessages/update_allowance_payload.go index ee0d677c0e7..c0aa957934a 100644 --- a/pkg/gen/ghcmessages/update_allowance_payload.go +++ b/pkg/gen/ghcmessages/update_allowance_payload.go @@ -70,6 +70,10 @@ type UpdateAllowancePayload struct { // ub allowance // Example: 500 UbAllowance *int64 `json:"ubAllowance,omitempty"` + + // Indicates the weight restriction for the move to a particular location. + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this update allowance payload diff --git a/pkg/gen/internalapi/embedded_spec.go b/pkg/gen/internalapi/embedded_spec.go index c872ff075a7..35d92886247 100644 --- a/pkg/gen/internalapi/embedded_spec.go +++ b/pkg/gen/internalapi/embedded_spec.go @@ -4486,6 +4486,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weight_restriction": { + "description": "Indicates the weight restricted to a specific location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -6037,6 +6043,10 @@ func init() { }, "uploaded_orders": { "$ref": "#/definitions/Document" + }, + "weightRestriction": { + "type": "integer", + "x-nullable": true } } }, @@ -13615,6 +13625,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weight_restriction": { + "description": "Indicates the weight restricted to a specific location.", + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -15168,6 +15184,10 @@ func init() { }, "uploaded_orders": { "$ref": "#/definitions/Document" + }, + "weightRestriction": { + "type": "integer", + "x-nullable": true } } }, diff --git a/pkg/gen/internalmessages/entitlement.go b/pkg/gen/internalmessages/entitlement.go index 703b403758f..083ea350085 100644 --- a/pkg/gen/internalmessages/entitlement.go +++ b/pkg/gen/internalmessages/entitlement.go @@ -42,6 +42,10 @@ type Entitlement struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UbAllowance *int64 `json:"ub_allowance,omitempty"` + + // Indicates the weight restricted to a specific location. + // Example: 1500 + WeightRestriction *int64 `json:"weight_restriction,omitempty"` } // Validate validates this entitlement diff --git a/pkg/gen/internalmessages/orders.go b/pkg/gen/internalmessages/orders.go index 6a789163d32..f36800d8445 100644 --- a/pkg/gen/internalmessages/orders.go +++ b/pkg/gen/internalmessages/orders.go @@ -127,6 +127,9 @@ type Orders struct { // uploaded orders // Required: true UploadedOrders *Document `json:"uploaded_orders"` + + // weight restriction + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this orders diff --git a/pkg/gen/primeapi/embedded_spec.go b/pkg/gen/primeapi/embedded_spec.go index ab7b9225ce4..01c7e0976c2 100644 --- a/pkg/gen/primeapi/embedded_spec.go +++ b/pkg/gen/primeapi/embedded_spec.go @@ -1828,6 +1828,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -2324,6 +2330,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -2480,13 +2530,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", @@ -6899,6 +6950,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -7395,6 +7452,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -7551,13 +7652,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", diff --git a/pkg/gen/primemessages/entitlements.go b/pkg/gen/primemessages/entitlements.go index c51ada24273..65870bfa8e6 100644 --- a/pkg/gen/primemessages/entitlements.go +++ b/pkg/gen/primemessages/entitlements.go @@ -79,6 +79,10 @@ type Entitlements struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UnaccompaniedBaggageAllowance *int64 `json:"unaccompaniedBaggageAllowance,omitempty"` + + // weight restriction + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this entitlements diff --git a/pkg/gen/primemessages/m_t_o_service_item.go b/pkg/gen/primemessages/m_t_o_service_item.go index 5ed0f248ae0..028e219d5df 100644 --- a/pkg/gen/primemessages/m_t_o_service_item.go +++ b/pkg/gen/primemessages/m_t_o_service_item.go @@ -273,6 +273,12 @@ func unmarshalMTOServiceItem(data []byte, consumer runtime.Consumer) (MTOService return nil, err } return &result, nil + case "MTOServiceItemDomesticShuttle": + var result MTOServiceItemDomesticShuttle + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "MTOServiceItemInternationalCrating": var result MTOServiceItemInternationalCrating if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/gen/primemessages/m_t_o_service_item_domestic_shuttle.go b/pkg/gen/primemessages/m_t_o_service_item_domestic_shuttle.go new file mode 100644 index 00000000000..1d5e28daaae --- /dev/null +++ b/pkg/gen/primemessages/m_t_o_service_item_domestic_shuttle.go @@ -0,0 +1,633 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package primemessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// MTOServiceItemDomesticShuttle Describes a domestic shuttle service item. +// +// swagger:model MTOServiceItemDomesticShuttle +type MTOServiceItemDomesticShuttle struct { + eTagField string + + idField strfmt.UUID + + lockedPriceCentsField *int64 + + moveTaskOrderIdField *strfmt.UUID + + mtoShipmentIdField strfmt.UUID + + reServiceNameField string + + rejectionReasonField *string + + serviceRequestDocumentsField ServiceRequestDocuments + + statusField MTOServiceItemStatus + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` +} + +// ETag gets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) ETag() string { + return m.eTagField +} + +// SetETag sets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) SetETag(val string) { + m.eTagField = val +} + +// ID gets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) ID() strfmt.UUID { + return m.idField +} + +// SetID sets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) SetID(val strfmt.UUID) { + m.idField = val +} + +// LockedPriceCents gets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) LockedPriceCents() *int64 { + return m.lockedPriceCentsField +} + +// SetLockedPriceCents sets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetLockedPriceCents(val *int64) { + m.lockedPriceCentsField = val +} + +// ModelType gets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) ModelType() MTOServiceItemModelType { + return "MTOServiceItemDomesticShuttle" +} + +// SetModelType sets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) SetModelType(val MTOServiceItemModelType) { +} + +// MoveTaskOrderID gets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MoveTaskOrderID() *strfmt.UUID { + return m.moveTaskOrderIdField +} + +// SetMoveTaskOrderID sets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMoveTaskOrderID(val *strfmt.UUID) { + m.moveTaskOrderIdField = val +} + +// MtoShipmentID gets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MtoShipmentID() strfmt.UUID { + return m.mtoShipmentIdField +} + +// SetMtoShipmentID sets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMtoShipmentID(val strfmt.UUID) { + m.mtoShipmentIdField = val +} + +// ReServiceName gets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) ReServiceName() string { + return m.reServiceNameField +} + +// SetReServiceName sets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) SetReServiceName(val string) { + m.reServiceNameField = val +} + +// RejectionReason gets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) RejectionReason() *string { + return m.rejectionReasonField +} + +// SetRejectionReason sets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) SetRejectionReason(val *string) { + m.rejectionReasonField = val +} + +// ServiceRequestDocuments gets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) ServiceRequestDocuments() ServiceRequestDocuments { + return m.serviceRequestDocumentsField +} + +// SetServiceRequestDocuments sets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetServiceRequestDocuments(val ServiceRequestDocuments) { + m.serviceRequestDocumentsField = val +} + +// Status gets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) Status() MTOServiceItemStatus { + return m.statusField +} + +// SetStatus sets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) SetStatus(val MTOServiceItemStatus) { + m.statusField = val +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *MTOServiceItemDomesticShuttle) UnmarshalJSON(raw []byte) error { + var data struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + } + buf := bytes.NewBuffer(raw) + dec := json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&data); err != nil { + return err + } + + var base struct { + /* Just the base type fields. Used for unmashalling polymorphic types.*/ + + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + } + buf = bytes.NewBuffer(raw) + dec = json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&base); err != nil { + return err + } + + var result MTOServiceItemDomesticShuttle + + result.eTagField = base.ETag + + result.idField = base.ID + + result.lockedPriceCentsField = base.LockedPriceCents + + if base.ModelType != result.ModelType() { + /* Not the type we're looking for. */ + return errors.New(422, "invalid modelType value: %q", base.ModelType) + } + result.moveTaskOrderIdField = base.MoveTaskOrderID + + result.mtoShipmentIdField = base.MtoShipmentID + + result.reServiceNameField = base.ReServiceName + + result.rejectionReasonField = base.RejectionReason + + result.serviceRequestDocumentsField = base.ServiceRequestDocuments + + result.statusField = base.Status + + result.ActualWeight = data.ActualWeight + result.EstimatedWeight = data.EstimatedWeight + result.ReServiceCode = data.ReServiceCode + result.Reason = data.Reason + + *m = result + + return nil +} + +// MarshalJSON marshals this object with a polymorphic type to a JSON structure +func (m MTOServiceItemDomesticShuttle) MarshalJSON() ([]byte, error) { + var b1, b2, b3 []byte + var err error + b1, err = json.Marshal(struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + }{ + + ActualWeight: m.ActualWeight, + + EstimatedWeight: m.EstimatedWeight, + + ReServiceCode: m.ReServiceCode, + + Reason: m.Reason, + }) + if err != nil { + return nil, err + } + b2, err = json.Marshal(struct { + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + }{ + + ETag: m.ETag(), + + ID: m.ID(), + + LockedPriceCents: m.LockedPriceCents(), + + ModelType: m.ModelType(), + + MoveTaskOrderID: m.MoveTaskOrderID(), + + MtoShipmentID: m.MtoShipmentID(), + + ReServiceName: m.ReServiceName(), + + RejectionReason: m.RejectionReason(), + + ServiceRequestDocuments: m.ServiceRequestDocuments(), + + Status: m.Status(), + }) + if err != nil { + return nil, err + } + + return swag.ConcatJSON(b1, b2, b3), nil +} + +// Validate validates this m t o service item domestic shuttle +func (m *MTOServiceItemDomesticShuttle) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMoveTaskOrderID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMtoShipmentID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateServiceRequestDocuments(formats); err != nil { + res = append(res, err) + } + + if err := m.validateStatus(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReServiceCode(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReason(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID()) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMoveTaskOrderID(formats strfmt.Registry) error { + + if err := validate.Required("moveTaskOrderID", "body", m.MoveTaskOrderID()); err != nil { + return err + } + + if err := validate.FormatOf("moveTaskOrderID", "body", "uuid", m.MoveTaskOrderID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMtoShipmentID(formats strfmt.Registry) error { + + if swag.IsZero(m.MtoShipmentID()) { // not required + return nil + } + + if err := validate.FormatOf("mtoShipmentID", "body", "uuid", m.MtoShipmentID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateServiceRequestDocuments(formats strfmt.Registry) error { + + if swag.IsZero(m.ServiceRequestDocuments()) { // not required + return nil + } + + if err := m.ServiceRequestDocuments().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateStatus(formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +var mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["DOSHUT","DDSHUT"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum = append(mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, v) + } +} + +// property enum +func (m *MTOServiceItemDomesticShuttle) validateReServiceCodeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReServiceCode(formats strfmt.Registry) error { + + if err := validate.Required("reServiceCode", "body", m.ReServiceCode); err != nil { + return err + } + + // value enum + if err := m.validateReServiceCodeEnum("reServiceCode", "body", *m.ReServiceCode); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReason(formats strfmt.Registry) error { + + if err := validate.Required("reason", "body", m.Reason); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this m t o service item domestic shuttle based on the context it is used +func (m *MTOServiceItemDomesticShuttle) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateETag(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateReServiceName(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateRejectionReason(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateServiceRequestDocuments(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateStatus(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateETag(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "eTag", "body", string(m.ETag())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "id", "body", strfmt.UUID(m.ID())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateModelType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ModelType().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("modelType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("modelType") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateReServiceName(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "reServiceName", "body", string(m.ReServiceName())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateRejectionReason(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "rejectionReason", "body", m.RejectionReason()); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateServiceRequestDocuments(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ServiceRequestDocuments().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) UnmarshalBinary(b []byte) error { + var res MTOServiceItemDomesticShuttle + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/primemessages/m_t_o_service_item_model_type.go b/pkg/gen/primemessages/m_t_o_service_item_model_type.go index 9326c1377a1..3c494ac62f3 100644 --- a/pkg/gen/primemessages/m_t_o_service_item_model_type.go +++ b/pkg/gen/primemessages/m_t_o_service_item_model_type.go @@ -20,6 +20,7 @@ import ( // - DOFSIT, DOASIT - MTOServiceItemOriginSIT // - DDFSIT, DDASIT - MTOServiceItemDestSIT // - DOSHUT, DDSHUT - MTOServiceItemShuttle +// - DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle // - IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle // - DCRT, DUCRT - MTOServiceItemDomesticCrating // - ICRT, IUCRT - MTOServiceItemInternationalCrating @@ -53,6 +54,9 @@ const ( // MTOServiceItemModelTypeMTOServiceItemShuttle captures enum value "MTOServiceItemShuttle" MTOServiceItemModelTypeMTOServiceItemShuttle MTOServiceItemModelType = "MTOServiceItemShuttle" + // MTOServiceItemModelTypeMTOServiceItemDomesticShuttle captures enum value "MTOServiceItemDomesticShuttle" + MTOServiceItemModelTypeMTOServiceItemDomesticShuttle MTOServiceItemModelType = "MTOServiceItemDomesticShuttle" + // MTOServiceItemModelTypeMTOServiceItemInternationalShuttle captures enum value "MTOServiceItemInternationalShuttle" MTOServiceItemModelTypeMTOServiceItemInternationalShuttle MTOServiceItemModelType = "MTOServiceItemInternationalShuttle" @@ -71,7 +75,7 @@ var mTOServiceItemModelTypeEnum []interface{} func init() { var res []MTOServiceItemModelType - if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemDomesticShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/primev2api/embedded_spec.go b/pkg/gen/primev2api/embedded_spec.go index 8cbeaf84b17..8a15212de76 100644 --- a/pkg/gen/primev2api/embedded_spec.go +++ b/pkg/gen/primev2api/embedded_spec.go @@ -1064,6 +1064,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -1429,6 +1435,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -1560,13 +1610,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", @@ -4822,6 +4873,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -5187,6 +5244,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -5318,13 +5419,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", diff --git a/pkg/gen/primev2messages/entitlements.go b/pkg/gen/primev2messages/entitlements.go index e29d3f733e3..58280696ab1 100644 --- a/pkg/gen/primev2messages/entitlements.go +++ b/pkg/gen/primev2messages/entitlements.go @@ -79,6 +79,10 @@ type Entitlements struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UnaccompaniedBaggageAllowance *int64 `json:"unaccompaniedBaggageAllowance,omitempty"` + + // weight restriction + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this entitlements diff --git a/pkg/gen/primev2messages/m_t_o_service_item.go b/pkg/gen/primev2messages/m_t_o_service_item.go index 7dfadf4c428..c06f87e420c 100644 --- a/pkg/gen/primev2messages/m_t_o_service_item.go +++ b/pkg/gen/primev2messages/m_t_o_service_item.go @@ -273,6 +273,12 @@ func unmarshalMTOServiceItem(data []byte, consumer runtime.Consumer) (MTOService return nil, err } return &result, nil + case "MTOServiceItemDomesticShuttle": + var result MTOServiceItemDomesticShuttle + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "MTOServiceItemInternationalCrating": var result MTOServiceItemInternationalCrating if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/gen/primev2messages/m_t_o_service_item_domestic_shuttle.go b/pkg/gen/primev2messages/m_t_o_service_item_domestic_shuttle.go new file mode 100644 index 00000000000..c5767d8bddf --- /dev/null +++ b/pkg/gen/primev2messages/m_t_o_service_item_domestic_shuttle.go @@ -0,0 +1,633 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package primev2messages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// MTOServiceItemDomesticShuttle Describes a domestic shuttle service item. +// +// swagger:model MTOServiceItemDomesticShuttle +type MTOServiceItemDomesticShuttle struct { + eTagField string + + idField strfmt.UUID + + lockedPriceCentsField *int64 + + moveTaskOrderIdField *strfmt.UUID + + mtoShipmentIdField strfmt.UUID + + reServiceNameField string + + rejectionReasonField *string + + serviceRequestDocumentsField ServiceRequestDocuments + + statusField MTOServiceItemStatus + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` +} + +// ETag gets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) ETag() string { + return m.eTagField +} + +// SetETag sets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) SetETag(val string) { + m.eTagField = val +} + +// ID gets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) ID() strfmt.UUID { + return m.idField +} + +// SetID sets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) SetID(val strfmt.UUID) { + m.idField = val +} + +// LockedPriceCents gets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) LockedPriceCents() *int64 { + return m.lockedPriceCentsField +} + +// SetLockedPriceCents sets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetLockedPriceCents(val *int64) { + m.lockedPriceCentsField = val +} + +// ModelType gets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) ModelType() MTOServiceItemModelType { + return "MTOServiceItemDomesticShuttle" +} + +// SetModelType sets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) SetModelType(val MTOServiceItemModelType) { +} + +// MoveTaskOrderID gets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MoveTaskOrderID() *strfmt.UUID { + return m.moveTaskOrderIdField +} + +// SetMoveTaskOrderID sets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMoveTaskOrderID(val *strfmt.UUID) { + m.moveTaskOrderIdField = val +} + +// MtoShipmentID gets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MtoShipmentID() strfmt.UUID { + return m.mtoShipmentIdField +} + +// SetMtoShipmentID sets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMtoShipmentID(val strfmt.UUID) { + m.mtoShipmentIdField = val +} + +// ReServiceName gets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) ReServiceName() string { + return m.reServiceNameField +} + +// SetReServiceName sets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) SetReServiceName(val string) { + m.reServiceNameField = val +} + +// RejectionReason gets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) RejectionReason() *string { + return m.rejectionReasonField +} + +// SetRejectionReason sets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) SetRejectionReason(val *string) { + m.rejectionReasonField = val +} + +// ServiceRequestDocuments gets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) ServiceRequestDocuments() ServiceRequestDocuments { + return m.serviceRequestDocumentsField +} + +// SetServiceRequestDocuments sets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetServiceRequestDocuments(val ServiceRequestDocuments) { + m.serviceRequestDocumentsField = val +} + +// Status gets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) Status() MTOServiceItemStatus { + return m.statusField +} + +// SetStatus sets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) SetStatus(val MTOServiceItemStatus) { + m.statusField = val +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *MTOServiceItemDomesticShuttle) UnmarshalJSON(raw []byte) error { + var data struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + } + buf := bytes.NewBuffer(raw) + dec := json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&data); err != nil { + return err + } + + var base struct { + /* Just the base type fields. Used for unmashalling polymorphic types.*/ + + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + } + buf = bytes.NewBuffer(raw) + dec = json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&base); err != nil { + return err + } + + var result MTOServiceItemDomesticShuttle + + result.eTagField = base.ETag + + result.idField = base.ID + + result.lockedPriceCentsField = base.LockedPriceCents + + if base.ModelType != result.ModelType() { + /* Not the type we're looking for. */ + return errors.New(422, "invalid modelType value: %q", base.ModelType) + } + result.moveTaskOrderIdField = base.MoveTaskOrderID + + result.mtoShipmentIdField = base.MtoShipmentID + + result.reServiceNameField = base.ReServiceName + + result.rejectionReasonField = base.RejectionReason + + result.serviceRequestDocumentsField = base.ServiceRequestDocuments + + result.statusField = base.Status + + result.ActualWeight = data.ActualWeight + result.EstimatedWeight = data.EstimatedWeight + result.ReServiceCode = data.ReServiceCode + result.Reason = data.Reason + + *m = result + + return nil +} + +// MarshalJSON marshals this object with a polymorphic type to a JSON structure +func (m MTOServiceItemDomesticShuttle) MarshalJSON() ([]byte, error) { + var b1, b2, b3 []byte + var err error + b1, err = json.Marshal(struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + }{ + + ActualWeight: m.ActualWeight, + + EstimatedWeight: m.EstimatedWeight, + + ReServiceCode: m.ReServiceCode, + + Reason: m.Reason, + }) + if err != nil { + return nil, err + } + b2, err = json.Marshal(struct { + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + }{ + + ETag: m.ETag(), + + ID: m.ID(), + + LockedPriceCents: m.LockedPriceCents(), + + ModelType: m.ModelType(), + + MoveTaskOrderID: m.MoveTaskOrderID(), + + MtoShipmentID: m.MtoShipmentID(), + + ReServiceName: m.ReServiceName(), + + RejectionReason: m.RejectionReason(), + + ServiceRequestDocuments: m.ServiceRequestDocuments(), + + Status: m.Status(), + }) + if err != nil { + return nil, err + } + + return swag.ConcatJSON(b1, b2, b3), nil +} + +// Validate validates this m t o service item domestic shuttle +func (m *MTOServiceItemDomesticShuttle) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMoveTaskOrderID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMtoShipmentID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateServiceRequestDocuments(formats); err != nil { + res = append(res, err) + } + + if err := m.validateStatus(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReServiceCode(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReason(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID()) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMoveTaskOrderID(formats strfmt.Registry) error { + + if err := validate.Required("moveTaskOrderID", "body", m.MoveTaskOrderID()); err != nil { + return err + } + + if err := validate.FormatOf("moveTaskOrderID", "body", "uuid", m.MoveTaskOrderID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMtoShipmentID(formats strfmt.Registry) error { + + if swag.IsZero(m.MtoShipmentID()) { // not required + return nil + } + + if err := validate.FormatOf("mtoShipmentID", "body", "uuid", m.MtoShipmentID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateServiceRequestDocuments(formats strfmt.Registry) error { + + if swag.IsZero(m.ServiceRequestDocuments()) { // not required + return nil + } + + if err := m.ServiceRequestDocuments().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateStatus(formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +var mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["DOSHUT","DDSHUT"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum = append(mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, v) + } +} + +// property enum +func (m *MTOServiceItemDomesticShuttle) validateReServiceCodeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReServiceCode(formats strfmt.Registry) error { + + if err := validate.Required("reServiceCode", "body", m.ReServiceCode); err != nil { + return err + } + + // value enum + if err := m.validateReServiceCodeEnum("reServiceCode", "body", *m.ReServiceCode); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReason(formats strfmt.Registry) error { + + if err := validate.Required("reason", "body", m.Reason); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this m t o service item domestic shuttle based on the context it is used +func (m *MTOServiceItemDomesticShuttle) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateETag(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateReServiceName(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateRejectionReason(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateServiceRequestDocuments(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateStatus(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateETag(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "eTag", "body", string(m.ETag())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "id", "body", strfmt.UUID(m.ID())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateModelType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ModelType().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("modelType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("modelType") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateReServiceName(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "reServiceName", "body", string(m.ReServiceName())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateRejectionReason(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "rejectionReason", "body", m.RejectionReason()); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateServiceRequestDocuments(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ServiceRequestDocuments().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) UnmarshalBinary(b []byte) error { + var res MTOServiceItemDomesticShuttle + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/primev2messages/m_t_o_service_item_model_type.go b/pkg/gen/primev2messages/m_t_o_service_item_model_type.go index 97d0c5272dc..77247b78fe0 100644 --- a/pkg/gen/primev2messages/m_t_o_service_item_model_type.go +++ b/pkg/gen/primev2messages/m_t_o_service_item_model_type.go @@ -20,6 +20,7 @@ import ( // - DOFSIT, DOASIT - MTOServiceItemOriginSIT // - DDFSIT, DDASIT - MTOServiceItemDestSIT // - DOSHUT, DDSHUT - MTOServiceItemShuttle +// - DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle // - IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle // - DCRT, DUCRT - MTOServiceItemDomesticCrating // - ICRT, IUCRT - MTOServiceItemInternationalCrating @@ -53,6 +54,9 @@ const ( // MTOServiceItemModelTypeMTOServiceItemShuttle captures enum value "MTOServiceItemShuttle" MTOServiceItemModelTypeMTOServiceItemShuttle MTOServiceItemModelType = "MTOServiceItemShuttle" + // MTOServiceItemModelTypeMTOServiceItemDomesticShuttle captures enum value "MTOServiceItemDomesticShuttle" + MTOServiceItemModelTypeMTOServiceItemDomesticShuttle MTOServiceItemModelType = "MTOServiceItemDomesticShuttle" + // MTOServiceItemModelTypeMTOServiceItemInternationalShuttle captures enum value "MTOServiceItemInternationalShuttle" MTOServiceItemModelTypeMTOServiceItemInternationalShuttle MTOServiceItemModelType = "MTOServiceItemInternationalShuttle" @@ -71,7 +75,7 @@ var mTOServiceItemModelTypeEnum []interface{} func init() { var res []MTOServiceItemModelType - if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemDomesticShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/primev3api/embedded_spec.go b/pkg/gen/primev3api/embedded_spec.go index b936756cd78..b926e318a3e 100644 --- a/pkg/gen/primev3api/embedded_spec.go +++ b/pkg/gen/primev3api/embedded_spec.go @@ -1226,6 +1226,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -1591,6 +1597,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -1747,13 +1797,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", @@ -5694,6 +5745,12 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-formatting": "weight", + "x-nullable": true, + "example": 1500 } } }, @@ -6059,6 +6116,50 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a domestic shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode" + ], + "properties": { + "actualWeight": { + "description": "A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in the shuttling service.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "A unique code for the service item. Indicates if shuttling is requested for the shipment origin (` + "`" + `DOSHUT` + "`" + `) or destination (` + "`" + `DDSHUT` + "`" + `).\n", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item.\n", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemInternationalCrating": { "description": "Describes a international crating/uncrating service item subtype of a MTOServiceItem.", "allOf": [ @@ -6215,13 +6316,14 @@ func init() { ] }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n * PODFSC, POEFSC - MTOSerivceItemInternationalFuelSurcharge\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating", diff --git a/pkg/gen/primev3messages/entitlements.go b/pkg/gen/primev3messages/entitlements.go index 1e228c6350f..2ef73ccfbf5 100644 --- a/pkg/gen/primev3messages/entitlements.go +++ b/pkg/gen/primev3messages/entitlements.go @@ -79,6 +79,10 @@ type Entitlements struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UnaccompaniedBaggageAllowance *int64 `json:"unaccompaniedBaggageAllowance,omitempty"` + + // weight restriction + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this entitlements diff --git a/pkg/gen/primev3messages/m_t_o_service_item.go b/pkg/gen/primev3messages/m_t_o_service_item.go index 75d33c217f1..06f0d63c776 100644 --- a/pkg/gen/primev3messages/m_t_o_service_item.go +++ b/pkg/gen/primev3messages/m_t_o_service_item.go @@ -273,6 +273,12 @@ func unmarshalMTOServiceItem(data []byte, consumer runtime.Consumer) (MTOService return nil, err } return &result, nil + case "MTOServiceItemDomesticShuttle": + var result MTOServiceItemDomesticShuttle + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "MTOServiceItemInternationalCrating": var result MTOServiceItemInternationalCrating if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/gen/primev3messages/m_t_o_service_item_domestic_shuttle.go b/pkg/gen/primev3messages/m_t_o_service_item_domestic_shuttle.go new file mode 100644 index 00000000000..caea94b6010 --- /dev/null +++ b/pkg/gen/primev3messages/m_t_o_service_item_domestic_shuttle.go @@ -0,0 +1,633 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package primev3messages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// MTOServiceItemDomesticShuttle Describes a domestic shuttle service item. +// +// swagger:model MTOServiceItemDomesticShuttle +type MTOServiceItemDomesticShuttle struct { + eTagField string + + idField strfmt.UUID + + lockedPriceCentsField *int64 + + moveTaskOrderIdField *strfmt.UUID + + mtoShipmentIdField strfmt.UUID + + reServiceNameField string + + rejectionReasonField *string + + serviceRequestDocumentsField ServiceRequestDocuments + + statusField MTOServiceItemStatus + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` +} + +// ETag gets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) ETag() string { + return m.eTagField +} + +// SetETag sets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) SetETag(val string) { + m.eTagField = val +} + +// ID gets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) ID() strfmt.UUID { + return m.idField +} + +// SetID sets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) SetID(val strfmt.UUID) { + m.idField = val +} + +// LockedPriceCents gets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) LockedPriceCents() *int64 { + return m.lockedPriceCentsField +} + +// SetLockedPriceCents sets the locked price cents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetLockedPriceCents(val *int64) { + m.lockedPriceCentsField = val +} + +// ModelType gets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) ModelType() MTOServiceItemModelType { + return "MTOServiceItemDomesticShuttle" +} + +// SetModelType sets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) SetModelType(val MTOServiceItemModelType) { +} + +// MoveTaskOrderID gets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MoveTaskOrderID() *strfmt.UUID { + return m.moveTaskOrderIdField +} + +// SetMoveTaskOrderID sets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMoveTaskOrderID(val *strfmt.UUID) { + m.moveTaskOrderIdField = val +} + +// MtoShipmentID gets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MtoShipmentID() strfmt.UUID { + return m.mtoShipmentIdField +} + +// SetMtoShipmentID sets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMtoShipmentID(val strfmt.UUID) { + m.mtoShipmentIdField = val +} + +// ReServiceName gets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) ReServiceName() string { + return m.reServiceNameField +} + +// SetReServiceName sets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) SetReServiceName(val string) { + m.reServiceNameField = val +} + +// RejectionReason gets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) RejectionReason() *string { + return m.rejectionReasonField +} + +// SetRejectionReason sets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) SetRejectionReason(val *string) { + m.rejectionReasonField = val +} + +// ServiceRequestDocuments gets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) ServiceRequestDocuments() ServiceRequestDocuments { + return m.serviceRequestDocumentsField +} + +// SetServiceRequestDocuments sets the service request documents of this subtype +func (m *MTOServiceItemDomesticShuttle) SetServiceRequestDocuments(val ServiceRequestDocuments) { + m.serviceRequestDocumentsField = val +} + +// Status gets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) Status() MTOServiceItemStatus { + return m.statusField +} + +// SetStatus sets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) SetStatus(val MTOServiceItemStatus) { + m.statusField = val +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *MTOServiceItemDomesticShuttle) UnmarshalJSON(raw []byte) error { + var data struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + } + buf := bytes.NewBuffer(raw) + dec := json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&data); err != nil { + return err + } + + var base struct { + /* Just the base type fields. Used for unmashalling polymorphic types.*/ + + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + } + buf = bytes.NewBuffer(raw) + dec = json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&base); err != nil { + return err + } + + var result MTOServiceItemDomesticShuttle + + result.eTagField = base.ETag + + result.idField = base.ID + + result.lockedPriceCentsField = base.LockedPriceCents + + if base.ModelType != result.ModelType() { + /* Not the type we're looking for. */ + return errors.New(422, "invalid modelType value: %q", base.ModelType) + } + result.moveTaskOrderIdField = base.MoveTaskOrderID + + result.mtoShipmentIdField = base.MtoShipmentID + + result.reServiceNameField = base.ReServiceName + + result.rejectionReasonField = base.RejectionReason + + result.serviceRequestDocumentsField = base.ServiceRequestDocuments + + result.statusField = base.Status + + result.ActualWeight = data.ActualWeight + result.EstimatedWeight = data.EstimatedWeight + result.ReServiceCode = data.ReServiceCode + result.Reason = data.Reason + + *m = result + + return nil +} + +// MarshalJSON marshals this object with a polymorphic type to a JSON structure +func (m MTOServiceItemDomesticShuttle) MarshalJSON() ([]byte, error) { + var b1, b2, b3 []byte + var err error + b1, err = json.Marshal(struct { + + // A record of the actual weight that was shuttled. Provided by the movers, based on weight tickets. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in the shuttling service. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // A unique code for the service item. Indicates if shuttling is requested for the shipment origin (`DOSHUT`) or destination (`DDSHUT`). + // + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // The contractor's explanation for why a shuttle service is requested. Used by the TOO while deciding to approve or reject the service item. + // + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + }{ + + ActualWeight: m.ActualWeight, + + EstimatedWeight: m.EstimatedWeight, + + ReServiceCode: m.ReServiceCode, + + Reason: m.Reason, + }) + if err != nil { + return nil, err + } + b2, err = json.Marshal(struct { + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + LockedPriceCents *int64 `json:"lockedPriceCents,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + ServiceRequestDocuments ServiceRequestDocuments `json:"serviceRequestDocuments,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + }{ + + ETag: m.ETag(), + + ID: m.ID(), + + LockedPriceCents: m.LockedPriceCents(), + + ModelType: m.ModelType(), + + MoveTaskOrderID: m.MoveTaskOrderID(), + + MtoShipmentID: m.MtoShipmentID(), + + ReServiceName: m.ReServiceName(), + + RejectionReason: m.RejectionReason(), + + ServiceRequestDocuments: m.ServiceRequestDocuments(), + + Status: m.Status(), + }) + if err != nil { + return nil, err + } + + return swag.ConcatJSON(b1, b2, b3), nil +} + +// Validate validates this m t o service item domestic shuttle +func (m *MTOServiceItemDomesticShuttle) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMoveTaskOrderID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMtoShipmentID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateServiceRequestDocuments(formats); err != nil { + res = append(res, err) + } + + if err := m.validateStatus(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReServiceCode(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReason(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID()) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMoveTaskOrderID(formats strfmt.Registry) error { + + if err := validate.Required("moveTaskOrderID", "body", m.MoveTaskOrderID()); err != nil { + return err + } + + if err := validate.FormatOf("moveTaskOrderID", "body", "uuid", m.MoveTaskOrderID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMtoShipmentID(formats strfmt.Registry) error { + + if swag.IsZero(m.MtoShipmentID()) { // not required + return nil + } + + if err := validate.FormatOf("mtoShipmentID", "body", "uuid", m.MtoShipmentID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateServiceRequestDocuments(formats strfmt.Registry) error { + + if swag.IsZero(m.ServiceRequestDocuments()) { // not required + return nil + } + + if err := m.ServiceRequestDocuments().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateStatus(formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +var mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["DOSHUT","DDSHUT"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum = append(mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, v) + } +} + +// property enum +func (m *MTOServiceItemDomesticShuttle) validateReServiceCodeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReServiceCode(formats strfmt.Registry) error { + + if err := validate.Required("reServiceCode", "body", m.ReServiceCode); err != nil { + return err + } + + // value enum + if err := m.validateReServiceCodeEnum("reServiceCode", "body", *m.ReServiceCode); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReason(formats strfmt.Registry) error { + + if err := validate.Required("reason", "body", m.Reason); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this m t o service item domestic shuttle based on the context it is used +func (m *MTOServiceItemDomesticShuttle) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateETag(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateReServiceName(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateRejectionReason(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateServiceRequestDocuments(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateStatus(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateETag(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "eTag", "body", string(m.ETag())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "id", "body", strfmt.UUID(m.ID())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateModelType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ModelType().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("modelType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("modelType") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateReServiceName(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "reServiceName", "body", string(m.ReServiceName())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateRejectionReason(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "rejectionReason", "body", m.RejectionReason()); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateServiceRequestDocuments(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ServiceRequestDocuments().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("serviceRequestDocuments") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("serviceRequestDocuments") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) UnmarshalBinary(b []byte) error { + var res MTOServiceItemDomesticShuttle + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/primev3messages/m_t_o_service_item_model_type.go b/pkg/gen/primev3messages/m_t_o_service_item_model_type.go index 53d2a0450f6..4dea531b524 100644 --- a/pkg/gen/primev3messages/m_t_o_service_item_model_type.go +++ b/pkg/gen/primev3messages/m_t_o_service_item_model_type.go @@ -20,6 +20,7 @@ import ( // - DOFSIT, DOASIT - MTOServiceItemOriginSIT // - DDFSIT, DDASIT - MTOServiceItemDestSIT // - DOSHUT, DDSHUT - MTOServiceItemShuttle +// - DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle // - IOSHUT, IDSHUT - MTOServiceItemInternationalShuttle // - DCRT, DUCRT - MTOServiceItemDomesticCrating // - ICRT, IUCRT - MTOServiceItemInternationalCrating @@ -53,6 +54,9 @@ const ( // MTOServiceItemModelTypeMTOServiceItemShuttle captures enum value "MTOServiceItemShuttle" MTOServiceItemModelTypeMTOServiceItemShuttle MTOServiceItemModelType = "MTOServiceItemShuttle" + // MTOServiceItemModelTypeMTOServiceItemDomesticShuttle captures enum value "MTOServiceItemDomesticShuttle" + MTOServiceItemModelTypeMTOServiceItemDomesticShuttle MTOServiceItemModelType = "MTOServiceItemDomesticShuttle" + // MTOServiceItemModelTypeMTOServiceItemInternationalShuttle captures enum value "MTOServiceItemInternationalShuttle" MTOServiceItemModelTypeMTOServiceItemInternationalShuttle MTOServiceItemModelType = "MTOServiceItemInternationalShuttle" @@ -71,7 +75,7 @@ var mTOServiceItemModelTypeEnum []interface{} func init() { var res []MTOServiceItemModelType - if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemDomesticShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating","MTOSerivceItemInternationalFuelSurcharge"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/gen/supportapi/embedded_spec.go b/pkg/gen/supportapi/embedded_spec.go index a8fc82230f8..c799c342054 100644 --- a/pkg/gen/supportapi/embedded_spec.go +++ b/pkg/gen/supportapi/embedded_spec.go @@ -1183,6 +1183,11 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -1531,14 +1536,60 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode", + "description" + ], + "properties": { + "actualWeight": { + "description": "Provided by the movers, based on weight tickets. Relevant for shuttling (DDSHUT \u0026 DOSHUT) service items.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in a shuttling (DDSHUT \u0026 DOSHUT) service item.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "Service codes allowed for this model type.", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "Explanation of why a shuttle service is required.", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating" @@ -4051,6 +4102,11 @@ func init() { "type": "integer", "x-nullable": true, "example": 3 + }, + "weightRestriction": { + "type": "integer", + "x-nullable": true, + "example": 1500 } } }, @@ -4399,14 +4455,60 @@ func init() { } ] }, + "MTOServiceItemDomesticShuttle": { + "description": "Describes a shuttle service item.", + "allOf": [ + { + "$ref": "#/definitions/MTOServiceItem" + }, + { + "type": "object", + "required": [ + "reason", + "reServiceCode", + "description" + ], + "properties": { + "actualWeight": { + "description": "Provided by the movers, based on weight tickets. Relevant for shuttling (DDSHUT \u0026 DOSHUT) service items.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4000 + }, + "estimatedWeight": { + "description": "An estimate of how much weight from a shipment will be included in a shuttling (DDSHUT \u0026 DOSHUT) service item.", + "type": "integer", + "x-nullable": true, + "x-omitempty": false, + "example": 4200 + }, + "reServiceCode": { + "description": "Service codes allowed for this model type.", + "type": "string", + "enum": [ + "DOSHUT", + "DDSHUT" + ] + }, + "reason": { + "description": "Explanation of why a shuttle service is required.", + "type": "string", + "example": "Storage items need to be picked up." + } + } + } + ] + }, "MTOServiceItemModelType": { - "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n\nThe documentation will then update with the supported fields.\n", + "description": "Describes all model sub-types for a MTOServiceItem model.\n\nUsing this list, choose the correct modelType in the dropdown, corresponding to the service item type.\n * DOFSIT, DOASIT - MTOServiceItemOriginSIT\n * DDFSIT, DDASIT - MTOServiceItemDestSIT\n * DOSHUT, DDSHUT - MTOServiceItemShuttle\n * DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle\n * DCRT, DUCRT - MTOServiceItemDomesticCrating\n * ICRT, IUCRT - MTOServiceItemInternationalCrating\n\nThe documentation will then update with the supported fields.\n", "type": "string", "enum": [ "MTOServiceItemBasic", "MTOServiceItemOriginSIT", "MTOServiceItemDestSIT", "MTOServiceItemShuttle", + "MTOServiceItemDomesticShuttle", "MTOServiceItemInternationalShuttle", "MTOServiceItemDomesticCrating", "MTOServiceItemInternationalCrating" diff --git a/pkg/gen/supportmessages/entitlement.go b/pkg/gen/supportmessages/entitlement.go index 434ad8aeed3..f1fd0f5f8c9 100644 --- a/pkg/gen/supportmessages/entitlement.go +++ b/pkg/gen/supportmessages/entitlement.go @@ -81,6 +81,10 @@ type Entitlement struct { // The amount of weight in pounds that the move is entitled for shipment types of Unaccompanied Baggage. // Example: 3 UnaccompaniedBaggageAllowance *int64 `json:"unaccompaniedBaggageAllowance,omitempty"` + + // weight restriction + // Example: 1500 + WeightRestriction *int64 `json:"weightRestriction,omitempty"` } // Validate validates this entitlement diff --git a/pkg/gen/supportmessages/m_t_o_service_item.go b/pkg/gen/supportmessages/m_t_o_service_item.go index 23f35835eb2..67cd5524d3f 100644 --- a/pkg/gen/supportmessages/m_t_o_service_item.go +++ b/pkg/gen/supportmessages/m_t_o_service_item.go @@ -239,6 +239,12 @@ func unmarshalMTOServiceItem(data []byte, consumer runtime.Consumer) (MTOService return nil, err } return &result, nil + case "MTOServiceItemDomesticShuttle": + var result MTOServiceItemDomesticShuttle + if err := consumer.Consume(buf2, &result); err != nil { + return nil, err + } + return &result, nil case "MTOServiceItemOriginSIT": var result MTOServiceItemOriginSIT if err := consumer.Consume(buf2, &result); err != nil { diff --git a/pkg/gen/supportmessages/m_t_o_service_item_domestic_shuttle.go b/pkg/gen/supportmessages/m_t_o_service_item_domestic_shuttle.go new file mode 100644 index 00000000000..47b7ad784ce --- /dev/null +++ b/pkg/gen/supportmessages/m_t_o_service_item_domestic_shuttle.go @@ -0,0 +1,521 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package supportmessages + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// MTOServiceItemDomesticShuttle Describes a shuttle service item. +// +// swagger:model MTOServiceItemDomesticShuttle +type MTOServiceItemDomesticShuttle struct { + eTagField string + + idField strfmt.UUID + + moveTaskOrderIdField *strfmt.UUID + + mtoShipmentIdField strfmt.UUID + + reServiceNameField string + + rejectionReasonField *string + + statusField MTOServiceItemStatus + + // Provided by the movers, based on weight tickets. Relevant for shuttling (DDSHUT & DOSHUT) service items. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in a shuttling (DDSHUT & DOSHUT) service item. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // Service codes allowed for this model type. + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // Explanation of why a shuttle service is required. + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` +} + +// ETag gets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) ETag() string { + return m.eTagField +} + +// SetETag sets the e tag of this subtype +func (m *MTOServiceItemDomesticShuttle) SetETag(val string) { + m.eTagField = val +} + +// ID gets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) ID() strfmt.UUID { + return m.idField +} + +// SetID sets the id of this subtype +func (m *MTOServiceItemDomesticShuttle) SetID(val strfmt.UUID) { + m.idField = val +} + +// ModelType gets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) ModelType() MTOServiceItemModelType { + return "MTOServiceItemDomesticShuttle" +} + +// SetModelType sets the model type of this subtype +func (m *MTOServiceItemDomesticShuttle) SetModelType(val MTOServiceItemModelType) { +} + +// MoveTaskOrderID gets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MoveTaskOrderID() *strfmt.UUID { + return m.moveTaskOrderIdField +} + +// SetMoveTaskOrderID sets the move task order ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMoveTaskOrderID(val *strfmt.UUID) { + m.moveTaskOrderIdField = val +} + +// MtoShipmentID gets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) MtoShipmentID() strfmt.UUID { + return m.mtoShipmentIdField +} + +// SetMtoShipmentID sets the mto shipment ID of this subtype +func (m *MTOServiceItemDomesticShuttle) SetMtoShipmentID(val strfmt.UUID) { + m.mtoShipmentIdField = val +} + +// ReServiceName gets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) ReServiceName() string { + return m.reServiceNameField +} + +// SetReServiceName sets the re service name of this subtype +func (m *MTOServiceItemDomesticShuttle) SetReServiceName(val string) { + m.reServiceNameField = val +} + +// RejectionReason gets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) RejectionReason() *string { + return m.rejectionReasonField +} + +// SetRejectionReason sets the rejection reason of this subtype +func (m *MTOServiceItemDomesticShuttle) SetRejectionReason(val *string) { + m.rejectionReasonField = val +} + +// Status gets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) Status() MTOServiceItemStatus { + return m.statusField +} + +// SetStatus sets the status of this subtype +func (m *MTOServiceItemDomesticShuttle) SetStatus(val MTOServiceItemStatus) { + m.statusField = val +} + +// UnmarshalJSON unmarshals this object with a polymorphic type from a JSON structure +func (m *MTOServiceItemDomesticShuttle) UnmarshalJSON(raw []byte) error { + var data struct { + + // Provided by the movers, based on weight tickets. Relevant for shuttling (DDSHUT & DOSHUT) service items. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in a shuttling (DDSHUT & DOSHUT) service item. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // Service codes allowed for this model type. + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // Explanation of why a shuttle service is required. + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + } + buf := bytes.NewBuffer(raw) + dec := json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&data); err != nil { + return err + } + + var base struct { + /* Just the base type fields. Used for unmashalling polymorphic types.*/ + + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + } + buf = bytes.NewBuffer(raw) + dec = json.NewDecoder(buf) + dec.UseNumber() + + if err := dec.Decode(&base); err != nil { + return err + } + + var result MTOServiceItemDomesticShuttle + + result.eTagField = base.ETag + + result.idField = base.ID + + if base.ModelType != result.ModelType() { + /* Not the type we're looking for. */ + return errors.New(422, "invalid modelType value: %q", base.ModelType) + } + result.moveTaskOrderIdField = base.MoveTaskOrderID + + result.mtoShipmentIdField = base.MtoShipmentID + + result.reServiceNameField = base.ReServiceName + + result.rejectionReasonField = base.RejectionReason + + result.statusField = base.Status + + result.ActualWeight = data.ActualWeight + result.EstimatedWeight = data.EstimatedWeight + result.ReServiceCode = data.ReServiceCode + result.Reason = data.Reason + + *m = result + + return nil +} + +// MarshalJSON marshals this object with a polymorphic type to a JSON structure +func (m MTOServiceItemDomesticShuttle) MarshalJSON() ([]byte, error) { + var b1, b2, b3 []byte + var err error + b1, err = json.Marshal(struct { + + // Provided by the movers, based on weight tickets. Relevant for shuttling (DDSHUT & DOSHUT) service items. + // Example: 4000 + ActualWeight *int64 `json:"actualWeight"` + + // An estimate of how much weight from a shipment will be included in a shuttling (DDSHUT & DOSHUT) service item. + // Example: 4200 + EstimatedWeight *int64 `json:"estimatedWeight"` + + // Service codes allowed for this model type. + // Required: true + // Enum: [DOSHUT DDSHUT] + ReServiceCode *string `json:"reServiceCode"` + + // Explanation of why a shuttle service is required. + // Example: Storage items need to be picked up. + // Required: true + Reason *string `json:"reason"` + }{ + + ActualWeight: m.ActualWeight, + + EstimatedWeight: m.EstimatedWeight, + + ReServiceCode: m.ReServiceCode, + + Reason: m.Reason, + }) + if err != nil { + return nil, err + } + b2, err = json.Marshal(struct { + ETag string `json:"eTag,omitempty"` + + ID strfmt.UUID `json:"id,omitempty"` + + ModelType MTOServiceItemModelType `json:"modelType"` + + MoveTaskOrderID *strfmt.UUID `json:"moveTaskOrderID"` + + MtoShipmentID strfmt.UUID `json:"mtoShipmentID,omitempty"` + + ReServiceName string `json:"reServiceName,omitempty"` + + RejectionReason *string `json:"rejectionReason,omitempty"` + + Status MTOServiceItemStatus `json:"status,omitempty"` + }{ + + ETag: m.ETag(), + + ID: m.ID(), + + ModelType: m.ModelType(), + + MoveTaskOrderID: m.MoveTaskOrderID(), + + MtoShipmentID: m.MtoShipmentID(), + + ReServiceName: m.ReServiceName(), + + RejectionReason: m.RejectionReason(), + + Status: m.Status(), + }) + if err != nil { + return nil, err + } + + return swag.ConcatJSON(b1, b2, b3), nil +} + +// Validate validates this m t o service item domestic shuttle +func (m *MTOServiceItemDomesticShuttle) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMoveTaskOrderID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateMtoShipmentID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateStatus(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReServiceCode(formats); err != nil { + res = append(res, err) + } + + if err := m.validateReason(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID()) { // not required + return nil + } + + if err := validate.FormatOf("id", "body", "uuid", m.ID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMoveTaskOrderID(formats strfmt.Registry) error { + + if err := validate.Required("moveTaskOrderID", "body", m.MoveTaskOrderID()); err != nil { + return err + } + + if err := validate.FormatOf("moveTaskOrderID", "body", "uuid", m.MoveTaskOrderID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateMtoShipmentID(formats strfmt.Registry) error { + + if swag.IsZero(m.MtoShipmentID()) { // not required + return nil + } + + if err := validate.FormatOf("mtoShipmentID", "body", "uuid", m.MtoShipmentID().String(), formats); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateStatus(formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +var mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["DOSHUT","DDSHUT"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum = append(mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, v) + } +} + +// property enum +func (m *MTOServiceItemDomesticShuttle) validateReServiceCodeEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, mTOServiceItemDomesticShuttleTypeReServiceCodePropEnum, true); err != nil { + return err + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReServiceCode(formats strfmt.Registry) error { + + if err := validate.Required("reServiceCode", "body", m.ReServiceCode); err != nil { + return err + } + + // value enum + if err := m.validateReServiceCodeEnum("reServiceCode", "body", *m.ReServiceCode); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) validateReason(formats strfmt.Registry) error { + + if err := validate.Required("reason", "body", m.Reason); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this m t o service item domestic shuttle based on the context it is used +func (m *MTOServiceItemDomesticShuttle) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateETag(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateReServiceName(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateStatus(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateETag(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "eTag", "body", string(m.ETag())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateModelType(ctx context.Context, formats strfmt.Registry) error { + + if err := m.ModelType().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("modelType") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("modelType") + } + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateReServiceName(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "reServiceName", "body", string(m.ReServiceName())); err != nil { + return err + } + + return nil +} + +func (m *MTOServiceItemDomesticShuttle) contextValidateStatus(ctx context.Context, formats strfmt.Registry) error { + + if swag.IsZero(m.Status()) { // not required + return nil + } + + if err := m.Status().ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("status") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("status") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MTOServiceItemDomesticShuttle) UnmarshalBinary(b []byte) error { + var res MTOServiceItemDomesticShuttle + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/gen/supportmessages/m_t_o_service_item_model_type.go b/pkg/gen/supportmessages/m_t_o_service_item_model_type.go index 3f957023e8a..ac2cf879c35 100644 --- a/pkg/gen/supportmessages/m_t_o_service_item_model_type.go +++ b/pkg/gen/supportmessages/m_t_o_service_item_model_type.go @@ -20,6 +20,7 @@ import ( // - DOFSIT, DOASIT - MTOServiceItemOriginSIT // - DDFSIT, DDASIT - MTOServiceItemDestSIT // - DOSHUT, DDSHUT - MTOServiceItemShuttle +// - DOSHUT, DDSHUT - MTOServiceItemDomesticShuttle // - DCRT, DUCRT - MTOServiceItemDomesticCrating // - ICRT, IUCRT - MTOServiceItemInternationalCrating // @@ -51,6 +52,9 @@ const ( // MTOServiceItemModelTypeMTOServiceItemShuttle captures enum value "MTOServiceItemShuttle" MTOServiceItemModelTypeMTOServiceItemShuttle MTOServiceItemModelType = "MTOServiceItemShuttle" + // MTOServiceItemModelTypeMTOServiceItemDomesticShuttle captures enum value "MTOServiceItemDomesticShuttle" + MTOServiceItemModelTypeMTOServiceItemDomesticShuttle MTOServiceItemModelType = "MTOServiceItemDomesticShuttle" + // MTOServiceItemModelTypeMTOServiceItemInternationalShuttle captures enum value "MTOServiceItemInternationalShuttle" MTOServiceItemModelTypeMTOServiceItemInternationalShuttle MTOServiceItemModelType = "MTOServiceItemInternationalShuttle" @@ -66,7 +70,7 @@ var mTOServiceItemModelTypeEnum []interface{} func init() { var res []MTOServiceItemModelType - if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating"]`), &res); err != nil { + if err := json.Unmarshal([]byte(`["MTOServiceItemBasic","MTOServiceItemOriginSIT","MTOServiceItemDestSIT","MTOServiceItemShuttle","MTOServiceItemDomesticShuttle","MTOServiceItemInternationalShuttle","MTOServiceItemDomesticCrating","MTOServiceItemInternationalCrating"]`), &res); err != nil { panic(err) } for _, v := range res { diff --git a/pkg/handlers/adminapi/api.go b/pkg/handlers/adminapi/api.go index 1dc3efa0343..5f8d2960b2e 100644 --- a/pkg/handlers/adminapi/api.go +++ b/pkg/handlers/adminapi/api.go @@ -53,16 +53,19 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { adminAPI.ServeError = handlers.ServeCustomError + transportationOfficeFetcher := transportationoffice.NewTransportationOfficesFetcher() + userRolesCreator := usersroles.NewUsersRolesCreator() + newRolesFetcher := roles.NewRolesFetcher() + adminAPI.RequestedOfficeUsersIndexRequestedOfficeUsersHandler = IndexRequestedOfficeUsersHandler{ handlerConfig, requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), query.NewQueryFilter, pagination.NewPagination, + transportationOfficeFetcher, + newRolesFetcher, } - userRolesCreator := usersroles.NewUsersRolesCreator() - newRolesFetcher := roles.NewRolesFetcher() - adminAPI.RequestedOfficeUsersGetRequestedOfficeUserHandler = GetRequestedOfficeUserHandler{ handlerConfig, requestedofficeusers.NewRequestedOfficeUserFetcher(queryBuilder), @@ -124,7 +127,6 @@ func NewAdminAPI(handlerConfig handlers.HandlerConfig) *adminops.MymoveAPI { pagination.NewPagination, } - transportationOfficeFetcher := transportationoffice.NewTransportationOfficesFetcher() adminAPI.TransportationOfficesGetOfficeByIDHandler = GetOfficeByIdHandler{ handlerConfig, transportationOfficeFetcher, diff --git a/pkg/handlers/adminapi/requested_office_users.go b/pkg/handlers/adminapi/requested_office_users.go index 5561fd7abf1..1571e8dbbc0 100644 --- a/pkg/handlers/adminapi/requested_office_users.go +++ b/pkg/handlers/adminapi/requested_office_users.go @@ -3,12 +3,14 @@ package adminapi import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" "strings" "github.com/go-openapi/runtime/middleware" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" "github.com/spf13/viper" "go.uber.org/zap" @@ -154,15 +156,29 @@ type IndexRequestedOfficeUsersHandler struct { services.RequestedOfficeUserListFetcher services.NewQueryFilter services.NewPagination + services.TransportationOfficesFetcher + services.RoleAssociater } -var requestedOfficeUserFilterConverters = map[string]func(string) []services.QueryFilter{ - "search": func(content string) []services.QueryFilter { - nameSearch := fmt.Sprintf("%s%%", content) - return []services.QueryFilter{ - query.NewQueryFilter("email", "ILIKE", fmt.Sprintf("%%%s%%", content)), - query.NewQueryFilter("first_name", "ILIKE", nameSearch), - query.NewQueryFilter("last_name", "ILIKE", nameSearch), +var requestedOfficeUserFilterConverters = map[string]func(string) func(*pop.Query){ + "search": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("office_users.email ILIKE ? AND office_users.status = 'REQUESTED' OR office_users.first_name ILIKE ? AND office_users.status = 'REQUESTED' OR office_users.last_name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch, nameSearch, nameSearch) + } + }, + + "offices": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("transportation_offices.name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch) + } + }, + + "rolesSearch": func(content string) func(*pop.Query) { + return func(query *pop.Query) { + nameSearch := fmt.Sprintf("%%%s%%", content) + query.Where("roles.role_name ILIKE ? AND office_users.status = 'REQUESTED'", nameSearch) } }, } @@ -172,27 +188,25 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - // adding in filters for when a search or filtering is done - queryFilters := generateQueryFilters(appCtx.Logger(), params.Filter, requestedOfficeUserFilterConverters) + var filtersMap map[string]string + if params.Filter != nil && *params.Filter != "" { + err := json.Unmarshal([]byte(*params.Filter), &filtersMap) + if err != nil { + return handlers.ResponseForError(appCtx.Logger(), errors.New("invalid filter format")), err + } + } - // We only want users that are in a REQUESTED status - queryFilters = append(queryFilters, query.NewQueryFilter("status", "=", "REQUESTED")) + var filterFuncs []func(*pop.Query) + for key, filterFunc := range requestedOfficeUserFilterConverters { + if filterValue, exists := filtersMap[key]; exists { + filterFuncs = append(filterFuncs, filterFunc(filterValue)) + } + } - // adding in pagination for the UI pagination := h.NewPagination(params.Page, params.PerPage) ordering := query.NewQueryOrder(params.Sort, params.Order) - // need to also get the user's roles - queryAssociations := query.NewQueryAssociationsPreload([]services.QueryAssociation{ - query.NewQueryAssociation("User.Roles"), - }) - - officeUsers, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersList(appCtx, queryFilters, queryAssociations, pagination, ordering) - if err != nil { - return handlers.ResponseForError(appCtx.Logger(), err), err - } - - totalOfficeUsersCount, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersCount(appCtx, queryFilters) + officeUsers, count, err := h.RequestedOfficeUserListFetcher.FetchRequestedOfficeUsersList(appCtx, filterFuncs, pagination, ordering) if err != nil { return handlers.ResponseForError(appCtx.Logger(), err), err } @@ -205,7 +219,7 @@ func (h IndexRequestedOfficeUsersHandler) Handle(params requested_office_users.I payload[i] = payloadForRequestedOfficeUserModel(s) } - return requested_office_users.NewIndexRequestedOfficeUsersOK().WithContentRange(fmt.Sprintf("requested office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, totalOfficeUsersCount)).WithPayload(payload), nil + return requested_office_users.NewIndexRequestedOfficeUsersOK().WithContentRange(fmt.Sprintf("requested office users %d-%d/%d", pagination.Offset(), pagination.Offset()+queriedOfficeUsersCount, count)).WithPayload(payload), nil }) } diff --git a/pkg/handlers/adminapi/requested_office_users_test.go b/pkg/handlers/adminapi/requested_office_users_test.go index d84c87ad69c..38f1c9c8afd 100644 --- a/pkg/handlers/adminapi/requested_office_users_test.go +++ b/pkg/handlers/adminapi/requested_office_users_test.go @@ -25,9 +25,7 @@ import ( ) func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { - // test that everything is wired up suite.Run("requested users result in ok response", func() { - // building two office user with requested status requestedOfficeUsers := models.OfficeUsers{ factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}), factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae})} @@ -45,16 +43,377 @@ func (suite *HandlerSuite) TestIndexRequestedOfficeUsersHandler() { response := handler.Handle(params) - // should get an ok response suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) suite.Len(okResponse.Payload, 2) - suite.Equal(requestedOfficeUsers[0].ID.String(), okResponse.Payload[0].ID.String()) + requestedOfficeUser1Id := requestedOfficeUsers[0].ID.String() + requestedOfficeUser2Id := requestedOfficeUsers[1].ID.String() + payloadRequestedUser1Id := okResponse.Payload[0].ID.String() + payloadRequestedUser2Id := okResponse.Payload[1].ID.String() + + // requested office users should exist in response no matter the ordering that has been applied + user1ExistsInResponse := requestedOfficeUser1Id == payloadRequestedUser1Id || requestedOfficeUser1Id == payloadRequestedUser2Id + user2ExistsInResponse := requestedOfficeUser2Id == payloadRequestedUser1Id || requestedOfficeUser2Id == payloadRequestedUser2Id + suite.True(user1ExistsInResponse) + suite.True(user2ExistsInResponse) + }) + + suite.Run("able to search by name & email", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // partial first name search + filterJSON := "{\"search\":\"Angel\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + + // search by first name + filterJSON = "{\"search\":\"Bill\"}" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[0].ID.String()) + + // email search + filterJSON = "{\"search\":\"conAir\"}" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[0].ID.String()) + }) + + suite.Run("test the return of sorted requested office users in asc order", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Kirtland AFB - USAF", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Fort Knox - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Detroit Arsenal - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + sortColumn := "transportation_office_id" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[2].ID.String()) + + // sort by transportation office name in desc order + sortColumn = "transportation_office_id" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(false), + } + + queryBuilder = query.NewQueryBuilder() + handler = IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[2].ID.String()) + + // sort by first name in asc order + sortColumn = "first_name" + params = requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder = query.NewQueryBuilder() + handler = IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response = handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse = response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 3) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + suite.Equal(officeUser2.ID.String(), okResponse.Payload[1].ID.String()) + suite.Equal(officeUser3.ID.String(), okResponse.Payload[2].ID.String()) + }) + + suite.Run("able to search by transportation office", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + Name: "Tinker", + }, + }, + }, nil) + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + TransportationOfficeID: transportationOffice.ID, + Status: &requestedStatus, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeTOO}) + factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeTIO}) + + filterJSON := "{\"offices\":\"Tinker\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + }) + + suite.Run("able to search by role", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + filterJSON := "{\"rolesSearch\":\"services\"}" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&requestedofficeuserop.IndexRequestedOfficeUsersOK{}, response) + okResponse := response.(*requestedofficeuserop.IndexRequestedOfficeUsersOK) + suite.Len(okResponse.Payload, 1) + suite.Equal(officeUser1.ID.String(), okResponse.Payload[0].ID.String()) + }) + + suite.Run("return error when querying for unhandled data", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + sortColumn := "unknown_column" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Sort: &sortColumn, + Order: models.BoolPointer(true), + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + suite.IsType(&handlers.ErrResponse{}, response) + errResponse := response.(*handlers.ErrResponse) + suite.Equal(http.StatusInternalServerError, errResponse.Code) + errMsg := errResponse.Err.Error() + suite.Equal(errMsg, "Unhandled data error encountered") + }) + + suite.Run("should error when a param filter format is incorrect", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + // Invalid format for filter params + filterJSON := "test{\"unknown\":\"value\"}test" + params := requestedofficeuserop.IndexRequestedOfficeUsersParams{ + HTTPRequest: suite.setupAuthenticatedRequest("GET", "/requested_office_users"), + Filter: &filterJSON, + } + + queryBuilder := query.NewQueryBuilder() + handler := IndexRequestedOfficeUsersHandler{ + HandlerConfig: suite.HandlerConfig(), + NewQueryFilter: query.NewQueryFilter, + RequestedOfficeUserListFetcher: requestedofficeusers.NewRequestedOfficeUsersListFetcher(queryBuilder), + NewPagination: pagination.NewPagination, + } + + response := handler.Handle(params) + + expectedError := models.ErrInvalidFilterFormat + expectedResponse := &handlers.ErrResponse{ + Code: http.StatusInternalServerError, + Err: expectedError, + } + + suite.Equal(expectedResponse, response) }) } func (suite *HandlerSuite) TestGetRequestedOfficeUserHandler() { - // test that everything is wired up suite.Run("integration test ok response", func() { requestedOfficeUser := factory.BuildOfficeUserWithRoles(suite.DB(), factory.GetTraitRequestedOfficeUser(), []roles.RoleType{roles.RoleTypeQae}) params := requestedofficeuserop.GetRequestedOfficeUserParams{ diff --git a/pkg/handlers/ghcapi/api.go b/pkg/handlers/ghcapi/api.go index b48c88402fc..e1e9c8a7b75 100644 --- a/pkg/handlers/ghcapi/api.go +++ b/pkg/handlers/ghcapi/api.go @@ -410,6 +410,20 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { mtoshipment.NewShipmentDeleter(moveTaskOrderUpdater, moveRouter), } + ghcAPI.ShipmentApproveShipmentsHandler = ApproveShipmentsHandler{ + handlerConfig, + mtoshipment.NewShipmentApprover( + mtoshipment.NewShipmentRouter(), + mtoserviceitem.NewMTOServiceItemCreator(handlerConfig.HHGPlanner(), queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + handlerConfig.HHGPlanner(), + move.NewMoveWeights(mtoshipment.NewShipmentReweighRequester(), waf), + moveTaskOrderUpdater, + moveRouter, + ), + shipmentSITStatus, + moveTaskOrderUpdater, + } + ghcAPI.ShipmentApproveShipmentHandler = ApproveShipmentHandler{ handlerConfig, mtoshipment.NewShipmentApprover( @@ -417,8 +431,11 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { mtoserviceitem.NewMTOServiceItemCreator(handlerConfig.HHGPlanner(), queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), handlerConfig.HHGPlanner(), move.NewMoveWeights(mtoshipment.NewShipmentReweighRequester(), waf), + moveTaskOrderUpdater, + moveRouter, ), shipmentSITStatus, + moveTaskOrderUpdater, } ghcAPI.ShipmentRequestShipmentDiversionHandler = RequestShipmentDiversionHandler{ @@ -665,6 +682,11 @@ func NewGhcAPIHandler(handlerConfig handlers.HandlerConfig) *ghcops.MymoveAPI { transportationOfficeFetcher, } + ghcAPI.TransportationOfficeShowCounselingOfficesHandler = ShowCounselingOfficesHandler{ + handlerConfig, + transportationOfficeFetcher, + } + ghcAPI.MoveUpdateCloseoutOfficeHandler = UpdateMoveCloseoutOfficeHandler{ handlerConfig, closeoutOfficeUpdater, diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 353e2e7c11e..7e9e05e90fe 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -122,6 +122,8 @@ func Move(move *models.Move, storer storage.FileStorer) (*ghcmessages.Move, erro SCAssignedUser: AssignedOfficeUser(move.SCAssignedUser), TOOAssignedUser: AssignedOfficeUser(move.TOOAssignedUser), TIOAssignedUser: AssignedOfficeUser(move.TIOAssignedUser), + CounselingOfficeID: handlers.FmtUUIDPtr(move.CounselingOfficeID), + CounselingOffice: TransportationOffice(move.CounselingOffice), } return payload, nil @@ -437,6 +439,18 @@ func GBLOCs(gblocs []string) ghcmessages.GBLOCs { return payload } +func CounselingOffices(counselingOffices models.TransportationOffices) ghcmessages.CounselingOffices { + payload := make(ghcmessages.CounselingOffices, len(counselingOffices)) + + for i, counselingOffice := range counselingOffices { + payload[i] = &ghcmessages.CounselingOffice{ + ID: handlers.FmtUUID(counselingOffice.ID), + Name: models.StringPointer(counselingOffice.Name), + } + } + return payload +} + // MoveHistory payload func MoveHistory(logger *zap.Logger, moveHistory *models.MoveHistory) *ghcmessages.MoveHistory { payload := &ghcmessages.MoveHistory{ @@ -733,6 +747,11 @@ func Entitlement(entitlement *models.Entitlement) *ghcmessages.Entitlements { if entitlement.UBAllowance != nil { ubAllowance = models.Int64Pointer(int64(*entitlement.UBAllowance)) } + var weightRestriction *int64 + if entitlement.WeightRestriction != nil { + weightRestriction = models.Int64Pointer(int64(*entitlement.WeightRestriction)) + } + return &ghcmessages.Entitlements{ ID: strfmt.UUID(entitlement.ID.String()), AuthorizedWeight: authorizedWeight, @@ -750,9 +769,11 @@ func Entitlement(entitlement *models.Entitlement) *ghcmessages.Entitlements { AccompaniedTour: accompaniedTour, UnaccompaniedBaggageAllowance: ubAllowance, OrganizationalClothingAndIndividualEquipment: entitlement.OrganizationalClothingAndIndividualEquipment, - GunSafe: gunSafe, - ETag: etag.GenerateEtag(entitlement.UpdatedAt), + GunSafe: gunSafe, + WeightRestriction: weightRestriction, + ETag: etag.GenerateEtag(entitlement.UpdatedAt), } + } // DutyLocation payload @@ -1300,13 +1321,16 @@ func PPMCloseout(ppmCloseout *models.PPMCloseout) *ghcmessages.PPMCloseout { Gcc: handlers.FmtCost(ppmCloseout.GCC), Aoa: handlers.FmtCost(ppmCloseout.AOA), RemainingIncentive: handlers.FmtCost(ppmCloseout.RemainingIncentive), - HaulType: (*string)(&ppmCloseout.HaulType), + HaulType: (*string)(ppmCloseout.HaulType), HaulPrice: handlers.FmtCost(ppmCloseout.HaulPrice), HaulFSC: handlers.FmtCost(ppmCloseout.HaulFSC), Dop: handlers.FmtCost(ppmCloseout.DOP), Ddp: handlers.FmtCost(ppmCloseout.DDP), PackPrice: handlers.FmtCost(ppmCloseout.PackPrice), UnpackPrice: handlers.FmtCost(ppmCloseout.UnpackPrice), + IntlPackPrice: handlers.FmtCost((ppmCloseout.IntlPackPrice)), + IntlUnpackPrice: handlers.FmtCost((ppmCloseout.IntlUnpackPrice)), + IntlLinehaulPrice: handlers.FmtCost((ppmCloseout.IntlLinehaulPrice)), SITReimbursement: handlers.FmtCost(ppmCloseout.SITReimbursement), } @@ -2207,12 +2231,12 @@ func BulkAssignmentData(appCtx appcontext.AppContext, moves []models.MoveWithEar return *bulkAssignmentData } -func queueMoveIsAssignable(move models.Move, assignedToUser *ghcmessages.AssignedOfficeUser, isCloseoutQueue bool, officeUser models.OfficeUser, ppmCloseoutGblocs bool) bool { +func queueMoveIsAssignable(move models.Move, assignedToUser *ghcmessages.AssignedOfficeUser, isCloseoutQueue bool, officeUser models.OfficeUser, ppmCloseoutGblocs bool, activeRole string) bool { // default to false isAssignable := false // HQ role is read only - if officeUser.User.Roles.HasRole(roles.RoleTypeHQ) { + if activeRole == string(roles.RoleTypeHQ) { isAssignable = false return isAssignable } @@ -2224,13 +2248,13 @@ func queueMoveIsAssignable(move models.Move, assignedToUser *ghcmessages.Assigne isSupervisor := officeUser.User.Privileges.HasPrivilege(models.PrivilegeTypeSupervisor) // in TOO queues, all moves are assignable for supervisor users - if officeUser.User.Roles.HasRole(roles.RoleTypeTOO) && isSupervisor { + if activeRole == string(roles.RoleTypeTOO) && isSupervisor { isAssignable = true } // if it is assigned in the SCs queue // it is only assignable if the user is a supervisor... - if officeUser.User.Roles.HasRole(roles.RoleTypeServicesCounselor) && isSupervisor { + if activeRole == string(roles.RoleTypeServicesCounselor) && isSupervisor { // AND we are in the counseling queue AND the move's counseling office is the supervisor's transportation office if !isCloseoutQueue && move.CounselingOfficeID != nil && *move.CounselingOfficeID == officeUser.TransportationOfficeID { isAssignable = true @@ -2250,35 +2274,36 @@ func queueMoveIsAssignable(move models.Move, assignedToUser *ghcmessages.Assigne } func servicesCounselorAvailableOfficeUsers(move models.Move, officeUsers []models.OfficeUser, officeUser models.OfficeUser, ppmCloseoutGblocs bool, isCloseoutQueue bool) []models.OfficeUser { - if officeUser.User.Roles.HasRole(roles.RoleTypeServicesCounselor) { - // if the office user currently assigned to the move works outside of the logged in users counseling office - // add them to the set - if move.SCAssignedUser != nil && move.SCAssignedUser.TransportationOfficeID != officeUser.TransportationOfficeID { - officeUsers = append(officeUsers, *move.SCAssignedUser) - } + // if the office user currently assigned to the move works outside of the logged in users counseling office + // add them to the set + if move.SCAssignedUser != nil && move.SCAssignedUser.TransportationOfficeID != officeUser.TransportationOfficeID { + officeUsers = append(officeUsers, *move.SCAssignedUser) + } - // if there is no counseling office - // OR if our current user doesn't work at the move's counseling office - // only available user should be themself - if !isCloseoutQueue && (move.CounselingOfficeID == nil) || (move.CounselingOfficeID != nil && *move.CounselingOfficeID != officeUser.TransportationOfficeID) { - officeUsers = models.OfficeUsers{officeUser} - } + var onlySelfAssign bool - // if its the closeout queue and its not a Navy, Marine, or Coast Guard user - // and the move doesn't have a closeout office - // OR the move's closeout office is not the office users office - // only available user should be themself - if isCloseoutQueue && !ppmCloseoutGblocs && move.CloseoutOfficeID == nil || (move.CloseoutOfficeID != nil && *move.CloseoutOfficeID != officeUser.TransportationOfficeID) { - officeUsers = models.OfficeUsers{officeUser} + // if there is no counseling office + // OR if our current user doesn't work at the move's counseling office + // only available user should be themself + onlySelfAssign = (move.CounselingOfficeID == nil) || (move.CounselingOfficeID != nil && *move.CounselingOfficeID != officeUser.TransportationOfficeID) + if !isCloseoutQueue && onlySelfAssign { + officeUsers = models.OfficeUsers{officeUser} + } - } + // if its the closeout queue and its not a Navy, Marine, or Coast Guard user + // and the move doesn't have a closeout office + // OR the move's closeout office is not the office users office + // only available user should be themself + onlySelfAssign = (move.CloseoutOfficeID == nil) || (move.CloseoutOfficeID != nil && *move.CloseoutOfficeID != officeUser.TransportationOfficeID) + if isCloseoutQueue && !ppmCloseoutGblocs && onlySelfAssign { + officeUsers = models.OfficeUsers{officeUser} } return officeUsers } // QueueMoves payload -func QueueMoves(moves []models.Move, officeUsers []models.OfficeUser, requestedPpmStatus *models.PPMShipmentStatus, officeUser models.OfficeUser, officeUsersSafety []models.OfficeUser) *ghcmessages.QueueMoves { +func QueueMoves(moves []models.Move, officeUsers []models.OfficeUser, requestedPpmStatus *models.PPMShipmentStatus, officeUser models.OfficeUser, officeUsersSafety []models.OfficeUser, activeRole string) *ghcmessages.QueueMoves { queueMoves := make(ghcmessages.QueueMoves, len(moves)) for i, move := range moves { customer := move.Orders.ServiceMember @@ -2350,10 +2375,10 @@ func QueueMoves(moves []models.Move, officeUsers []models.OfficeUser, requestedP // determine if there is an assigned user var assignedToUser *ghcmessages.AssignedOfficeUser - if officeUser.User.Roles.HasRole(roles.RoleTypeServicesCounselor) && move.SCAssignedUser != nil { + if (activeRole == string(roles.RoleTypeServicesCounselor) || activeRole == string(roles.RoleTypeHQ)) && move.SCAssignedUser != nil { assignedToUser = AssignedOfficeUser(move.SCAssignedUser) } - if officeUser.User.Roles.HasRole(roles.RoleTypeTOO) && move.TOOAssignedUser != nil { + if (activeRole == string(roles.RoleTypeTOO) || activeRole == string(roles.RoleTypeHQ)) && move.TOOAssignedUser != nil { assignedToUser = AssignedOfficeUser(move.TOOAssignedUser) } @@ -2362,7 +2387,7 @@ func QueueMoves(moves []models.Move, officeUsers []models.OfficeUser, requestedP // requestedPpmStatus also represents if we are viewing the closeout queue isCloseoutQueue := requestedPpmStatus != nil && *requestedPpmStatus == models.PPMShipmentStatusNeedsCloseout // determine if the move is assignable - assignable := queueMoveIsAssignable(move, assignedToUser, isCloseoutQueue, officeUser, ppmCloseoutGblocs) + assignable := queueMoveIsAssignable(move, assignedToUser, isCloseoutQueue, officeUser, ppmCloseoutGblocs, activeRole) isSupervisor := officeUser.User.Privileges.HasPrivilege(models.PrivilegeTypeSupervisor) // only need to attach available office users if move is assignable @@ -2374,7 +2399,22 @@ func QueueMoves(moves []models.Move, officeUsers []models.OfficeUser, requestedP if isSupervisor && move.Orders.OrdersType == "SAFETY" { availableOfficeUsers = officeUsersSafety } - if officeUser.User.Roles.HasRole(roles.RoleTypeServicesCounselor) { + + // if the assigned user is not in the returned list of available users append them to the end + if (activeRole == string(roles.RoleTypeTOO)) && (move.TOOAssignedUser != nil) { + userFound := false + for _, officeUser := range availableOfficeUsers { + if officeUser.ID == *move.TOOAssignedID { + userFound = true + break + } + } + if !userFound { + availableOfficeUsers = append(availableOfficeUsers, *move.TOOAssignedUser) + } + } + + if activeRole == string(roles.RoleTypeServicesCounselor) { availableOfficeUsers = servicesCounselorAvailableOfficeUsers(move, availableOfficeUsers, officeUser, ppmCloseoutGblocs, isCloseoutQueue) } @@ -2475,7 +2515,7 @@ func queuePaymentRequestStatus(paymentRequest models.PaymentRequest) string { } // QueuePaymentRequests payload -func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers []models.OfficeUser, officeUser models.OfficeUser, officeUsersSafety []models.OfficeUser) *ghcmessages.QueuePaymentRequests { +func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers []models.OfficeUser, officeUser models.OfficeUser, officeUsersSafety []models.OfficeUser, activeRole string) *ghcmessages.QueuePaymentRequests { queuePaymentRequests := make(ghcmessages.QueuePaymentRequests, len(*paymentRequests)) @@ -2520,7 +2560,7 @@ func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers [ isAssignable = true } - if officeUser.User.Roles.HasRole(roles.RoleTypeHQ) { + if activeRole == string(roles.RoleTypeHQ) { isAssignable = false } @@ -2532,6 +2572,22 @@ func QueuePaymentRequests(paymentRequests *models.PaymentRequests, officeUsers [ if isSupervisor && orders.OrdersType == "SAFETY" { availableOfficeUsers = officeUsersSafety } + + // if the assigned user is not in the returned list of available users append them to the end + if paymentRequest.MoveTaskOrder.TIOAssignedUser != nil { + userFound := false + for _, officeUser := range availableOfficeUsers { + if officeUser.ID == paymentRequest.MoveTaskOrder.TIOAssignedUser.ID { + userFound = true + break + } + } + if !userFound { + availableOfficeUsers = append(availableOfficeUsers, *paymentRequest.MoveTaskOrder.TIOAssignedUser) + } + } + + // if they're not a supervisor and it is assignable, the only option should be themself if !isSupervisor { availableOfficeUsers = models.OfficeUsers{officeUser} } diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go index 5c883654ecf..e1f22b64ee8 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go @@ -221,7 +221,8 @@ func (suite *PayloadsSuite) TestPaymentRequestQueue() { var officeUsers models.OfficeUsers var officeUsersSafety models.OfficeUsers officeUsers = append(officeUsers, officeUser) - var paymentRequestsQueue = QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety) + activeRole := string(roles.RoleTypeTIO) + var paymentRequestsQueue = QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety, activeRole) suite.Run("Test Payment request is assignable due to not being assigend", func() { paymentRequestCopy := *paymentRequestsQueue @@ -240,7 +241,7 @@ func (suite *PayloadsSuite) TestPaymentRequestQueue() { paymentRequests[0].MoveTaskOrder.TIOAssignedUser = &officeUserTIO paymentRequests[0].MoveTaskOrder.CounselingOffice = &transportationOffice - paymentRequestsQueue = QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety) + paymentRequestsQueue = QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety, activeRole) suite.Run("Test PaymentRequest has both Counseling Office and TIO AssignedUser ", func() { PaymentRequestsCopy := *paymentRequestsQueue @@ -254,14 +255,14 @@ func (suite *PayloadsSuite) TestPaymentRequestQueue() { }) suite.Run("Test PaymentRequest is assignable due to user Supervisor role", func() { - paymentRequests := QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety) + paymentRequests := QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety, activeRole) paymentRequestCopy := *paymentRequests suite.Equal(paymentRequestCopy[0].Assignable, true) }) - officeUserHQ := factory.BuildOfficeUserWithRoles(suite.DB(), nil, []roles.RoleType{roles.RoleTypeHQ}) + activeRole = string(roles.RoleTypeHQ) suite.Run("Test PaymentRequest is not assignable due to user HQ role", func() { - paymentRequests := QueuePaymentRequests(&paymentRequests, officeUsers, officeUserHQ, officeUsersSafety) + paymentRequests := QueuePaymentRequests(&paymentRequests, officeUsers, officeUser, officeUsersSafety, activeRole) paymentRequestCopy := *paymentRequests suite.Equal(paymentRequestCopy[0].Assignable, false) }) @@ -667,6 +668,7 @@ func (suite *PayloadsSuite) TestEntitlement() { dependentsTwelveAndOver := 1 authorizedWeight := 8000 ubAllowance := 300 + weightRestriction := 1000 entitlement := &models.Entitlement{ ID: entitlementID, @@ -684,6 +686,7 @@ func (suite *PayloadsSuite) TestEntitlement() { DependentsTwelveAndOver: &dependentsTwelveAndOver, UpdatedAt: time.Now(), UBAllowance: &ubAllowance, + WeightRestriction: &weightRestriction, } returnedEntitlement := Entitlement(entitlement) @@ -705,6 +708,7 @@ func (suite *PayloadsSuite) TestEntitlement() { suite.Equal(models.BoolPointer(accompaniedTour), returnedEntitlement.AccompaniedTour) suite.Equal(dependentsUnderTwelve, int(*returnedEntitlement.DependentsUnderTwelve)) suite.Equal(dependentsTwelveAndOver, int(*returnedEntitlement.DependentsTwelveAndOver)) + suite.Equal(weightRestriction, int(*returnedEntitlement.WeightRestriction)) } func (suite *PayloadsSuite) TestCreateCustomer() { @@ -1681,6 +1685,83 @@ func (suite *PayloadsSuite) TestMTOShipment_POE_POD_Locations() { }) } +func (suite *PayloadsSuite) TestPPMCloseout() { + plannedMoveDate := time.Now() + actualMoveDate := time.Now() + miles := 1200 + estimatedWeight := unit.Pound(5000) + actualWeight := unit.Pound(5200) + proGearWeightCustomer := unit.Pound(300) + proGearWeightSpouse := unit.Pound(100) + grossIncentive := unit.Cents(100000) + gcc := unit.Cents(50000) + aoa := unit.Cents(20000) + remainingIncentive := unit.Cents(30000) + haulType := "Linehaul" + haulPrice := unit.Cents(40000) + haulFSC := unit.Cents(5000) + dop := unit.Cents(10000) + ddp := unit.Cents(8000) + packPrice := unit.Cents(7000) + unpackPrice := unit.Cents(6000) + intlPackPrice := unit.Cents(15000) + intlUnpackPrice := unit.Cents(14000) + intlLinehaulPrice := unit.Cents(13000) + sitReimbursement := unit.Cents(12000) + + ppmCloseout := models.PPMCloseout{ + ID: models.UUIDPointer(uuid.Must(uuid.NewV4())), + PlannedMoveDate: &plannedMoveDate, + ActualMoveDate: &actualMoveDate, + Miles: &miles, + EstimatedWeight: &estimatedWeight, + ActualWeight: &actualWeight, + ProGearWeightCustomer: &proGearWeightCustomer, + ProGearWeightSpouse: &proGearWeightSpouse, + GrossIncentive: &grossIncentive, + GCC: &gcc, + AOA: &aoa, + RemainingIncentive: &remainingIncentive, + HaulType: (*models.HaulType)(&haulType), + HaulPrice: &haulPrice, + HaulFSC: &haulFSC, + DOP: &dop, + DDP: &ddp, + PackPrice: &packPrice, + UnpackPrice: &unpackPrice, + IntlPackPrice: &intlPackPrice, + IntlUnpackPrice: &intlUnpackPrice, + IntlLinehaulPrice: &intlLinehaulPrice, + SITReimbursement: &sitReimbursement, + } + + payload := PPMCloseout(&ppmCloseout) + suite.NotNil(payload) + suite.Equal(ppmCloseout.ID.String(), payload.ID.String()) + suite.Equal(handlers.FmtDatePtr(ppmCloseout.PlannedMoveDate), payload.PlannedMoveDate) + suite.Equal(handlers.FmtDatePtr(ppmCloseout.ActualMoveDate), payload.ActualMoveDate) + suite.Equal(handlers.FmtIntPtrToInt64(ppmCloseout.Miles), payload.Miles) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.EstimatedWeight), payload.EstimatedWeight) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ActualWeight), payload.ActualWeight) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ProGearWeightCustomer), payload.ProGearWeightCustomer) + suite.Equal(handlers.FmtPoundPtr(ppmCloseout.ProGearWeightSpouse), payload.ProGearWeightSpouse) + suite.Equal(handlers.FmtCost(ppmCloseout.GrossIncentive), payload.GrossIncentive) + suite.Equal(handlers.FmtCost(ppmCloseout.GCC), payload.Gcc) + suite.Equal(handlers.FmtCost(ppmCloseout.AOA), payload.Aoa) + suite.Equal(handlers.FmtCost(ppmCloseout.RemainingIncentive), payload.RemainingIncentive) + suite.Equal((*string)(ppmCloseout.HaulType), payload.HaulType) + suite.Equal(handlers.FmtCost(ppmCloseout.HaulPrice), payload.HaulPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.HaulFSC), payload.HaulFSC) + suite.Equal(handlers.FmtCost(ppmCloseout.DOP), payload.Dop) + suite.Equal(handlers.FmtCost(ppmCloseout.DDP), payload.Ddp) + suite.Equal(handlers.FmtCost(ppmCloseout.PackPrice), payload.PackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.UnpackPrice), payload.UnpackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlPackPrice), payload.IntlPackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlUnpackPrice), payload.IntlUnpackPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.IntlLinehaulPrice), payload.IntlLinehaulPrice) + suite.Equal(handlers.FmtCost(ppmCloseout.SITReimbursement), payload.SITReimbursement) +} + func (suite *PayloadsSuite) TestPaymentServiceItemPayload() { mtoServiceItemID := uuid.Must(uuid.NewV4()) mtoShipmentID := uuid.Must(uuid.NewV4()) @@ -1836,3 +1917,34 @@ func (suite *PayloadsSuite) TestPaymentServiceItemsPayload() { suite.Nil(psItem2.TppsInvoiceAmountPaidPerServiceItemMillicents) }) } + +func (suite *PayloadsSuite) TestCounselingOffices() { + suite.Run("correctly maps transportaion offices to counseling offices payload", func() { + office1 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Liberty", + }, + }, + }, nil) + + office2 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Walker", + }, + }, + }, nil) + + offices := models.TransportationOffices{office1, office2} + + payload := CounselingOffices(offices) + + suite.IsType(payload, ghcmessages.CounselingOffices{}) + suite.Equal(2, len(payload)) + suite.Equal(office1.ID.String(), payload[0].ID.String()) + suite.Equal(office2.ID.String(), payload[1].ID.String()) + }) +} diff --git a/pkg/handlers/ghcapi/move_task_order.go b/pkg/handlers/ghcapi/move_task_order.go index 2c9196b68be..5edab32d99a 100644 --- a/pkg/handlers/ghcapi/move_task_order.go +++ b/pkg/handlers/ghcapi/move_task_order.go @@ -17,7 +17,6 @@ import ( "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/audit" "github.com/transcom/mymove/pkg/services/event" - movetaskorder "github.com/transcom/mymove/pkg/services/move_task_order" ) // GetMoveTaskOrderHandler fetches a Move Task Order @@ -74,13 +73,7 @@ func (h UpdateMoveTaskOrderStatusHandlerFunc) Handle(params movetaskorderops.Upd serviceItemCodes = *params.ServiceItemCodes } - checker := movetaskorder.NewMoveTaskOrderChecker() - availableBefore, err := checker.MTOAvailableToPrime(appCtx, moveTaskOrderID) - if err != nil { - return movetaskorderops.NewUpdateMoveTaskOrderStatusInternalServerError(), err - } - - mto, err := h.moveTaskOrderStatusUpdater.MakeAvailableToPrime(appCtx, moveTaskOrderID, eTag, + mto, err := h.moveTaskOrderStatusUpdater.ApproveMoveAndCreateServiceItems(appCtx, moveTaskOrderID, eTag, serviceItemCodes.ServiceCodeMS, serviceItemCodes.ServiceCodeCS) if err != nil { @@ -106,35 +99,11 @@ func (h UpdateMoveTaskOrderStatusHandlerFunc) Handle(params movetaskorderops.Upd } } - if !availableBefore { - availableAfter, checkErr := checker.MTOAvailableToPrime(appCtx, moveTaskOrderID) - if checkErr != nil { - return movetaskorderops.NewUpdateMoveTaskOrderStatusInternalServerError(), err - } - - /* Do not send TOO approving and submitting service items email if BLUEBARK/SAFETY */ - if availableAfter && mto.Orders.CanSendEmailWithOrdersType() { - emailErr := h.NotificationSender().SendNotification(appCtx, - notifications.NewMoveIssuedToPrime(moveTaskOrderID), - ) - if emailErr != nil { - return movetaskorderops.NewUpdateMoveTaskOrderStatusInternalServerError(), err - } - } - } - moveTaskOrderPayload, err := payloads.Move(mto, h.FileStorer()) if err != nil { return movetaskorderops.NewUpdateMoveTaskOrderStatusInternalServerError(), err } - // Audit attempt to make MTO available to prime - _, err = audit.Capture(appCtx, mto, moveTaskOrderPayload, params.HTTPRequest) - if err != nil { - appCtx.Logger().Error("Auditing service error for making MTO available to Prime.", zap.Error(err)) - return movetaskorderops.NewUpdateMoveTaskOrderStatusInternalServerError(), err - } - _, err = event.TriggerEvent(event.Event{ EventKey: event.MoveTaskOrderUpdateEventKey, MtoID: mto.ID, diff --git a/pkg/handlers/ghcapi/move_task_order_test.go b/pkg/handlers/ghcapi/move_task_order_test.go index 316eae9b0c8..2500e841f51 100644 --- a/pkg/handlers/ghcapi/move_task_order_test.go +++ b/pkg/handlers/ghcapi/move_task_order_test.go @@ -217,9 +217,7 @@ func (suite *HandlerSuite) TestUpdateMoveTaskOrderHandlerIntegrationSuccess() { suite.Assertions.IsType(&movetaskorderops.UpdateMoveTaskOrderStatusOK{}, response) suite.Equal(strfmt.UUID(move.ID.String()), movePayload.ID) - suite.NotNil(movePayload.AvailableToPrimeAt) suite.NotNil(movePayload.ApprovedAt) - suite.HasWebhookNotification(move.ID, traceID) // this action always creates a notification for the Prime // also check MTO level service items are properly created var serviceItems models.MTOServiceItems @@ -263,7 +261,7 @@ func (suite *HandlerSuite) TestUpdateMoveTaskOrderHandlerIntegrationWithStaleEta // Stale ETags are already unit tested in the move_task_order_updater_test, // so we can mock this here to speed up the test and avoid hitting the DB moveUpdater := &mocks.MoveTaskOrderUpdater{} - moveUpdater.On("MakeAvailableToPrime", + moveUpdater.On("ApproveMoveAndCreateServiceItems", mock.AnythingOfType("*appcontext.appContext"), mock.Anything, mock.Anything, diff --git a/pkg/handlers/ghcapi/mto_shipment.go b/pkg/handlers/ghcapi/mto_shipment.go index 53011950b49..1e5453049e7 100644 --- a/pkg/handlers/ghcapi/mto_shipment.go +++ b/pkg/handlers/ghcapi/mto_shipment.go @@ -23,6 +23,7 @@ import ( "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/notifications" "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/services/audit" "github.com/transcom/mymove/pkg/services/event" mtoshipment "github.com/transcom/mymove/pkg/services/mto_shipment" ppmshipment "github.com/transcom/mymove/pkg/services/ppmshipment" @@ -511,6 +512,7 @@ type ApproveShipmentHandler struct { handlers.HandlerConfig services.ShipmentApprover services.ShipmentSITStatus + services.MoveTaskOrderUpdater } // Handle approves a shipment @@ -551,6 +553,49 @@ func (h ApproveShipmentHandler) Handle(params shipmentops.ApproveShipmentParams) return handleError(err) } + move, wasMadeAvailableToPrime, err := h.MakeAvailableToPrime(appCtx, shipment.MoveTaskOrderID) + if err != nil { + appCtx.Logger().Error("Error making move available to prime", zap.Error(err)) + return handleError(err) + } + + // Execute tasks if the move has just become available to Prime (migrated from move_task_order.go) + if wasMadeAvailableToPrime { + /* Do not send TOO approving and submitting service items email if BLUEBARK/SAFETY */ + if move.Orders.CanSendEmailWithOrdersType() { + emailErr := h.NotificationSender().SendNotification(appCtx, + notifications.NewMoveIssuedToPrime(move.ID), + ) + if emailErr != nil { + return handleError(err) + } + } + + // Prepare move payload for auditing + moveTaskOrderPayload, err := payloads.Move(move, h.FileStorer()) + if err != nil { + return handleError(err) + } + // Audit attempt to make MTO available to prime + _, err = audit.Capture(appCtx, move, moveTaskOrderPayload, params.HTTPRequest) + if err != nil { + appCtx.Logger().Error("Auditing service error for making MTO available to Prime.", zap.Error(err)) + return handleError(err) + } + // Move update event + _, err = event.TriggerEvent(event.Event{ + EventKey: event.MoveTaskOrderUpdateEventKey, + MtoID: move.ID, + UpdatedObjectID: move.ID, + EndpointKey: event.GhcUpdateMoveTaskOrderStatusEndpointKey, + AppContext: appCtx, + TraceID: h.GetTraceIDFromRequest(params.HTTPRequest), + }) + if err != nil { + appCtx.Logger().Error("ghcapi.ApproveShipmentHandlerFunc could not generate the event") + } + } + h.triggerShipmentApprovalEvent(appCtx, shipmentID, shipment.MoveTaskOrderID, params) shipmentSITStatus, _, err := h.CalculateShipmentSITStatus(appCtx, *shipment) @@ -582,6 +627,164 @@ func (h ApproveShipmentHandler) triggerShipmentApprovalEvent(appCtx appcontext.A } } +// ApproveShipmentsHandler approves one or more shipments +type ApproveShipmentsHandler struct { + handlers.HandlerConfig + services.ShipmentApprover + services.ShipmentSITStatus + services.MoveTaskOrderUpdater +} + +// Handle approves one or more shipments +func (h ApproveShipmentsHandler) Handle(params shipmentops.ApproveShipmentsParams) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + + // Check user permissions (TOO role required) + if !appCtx.Session().IsOfficeUser() || !appCtx.Session().Roles.HasRole(roles.RoleTypeTOO) { + forbiddenError := apperror.NewForbiddenError("Only TOO role can approve shipments") + appCtx.Logger().Error(forbiddenError.Error()) + return shipmentops.NewApproveShipmentsForbidden(), forbiddenError + } + + handleError := func(err error) (middleware.Responder, error) { + appCtx.Logger().Error("ghcapi.ApproveShipmentsHandler", zap.Error(err)) + payload := &ghcmessages.Error{Message: handlers.FmtString(err.Error())} + + switch e := err.(type) { + case apperror.NotFoundError: + return shipmentops.NewApproveShipmentsNotFound().WithPayload(payload), err + case apperror.InvalidInputError: + payload := payloadForValidationError("Validation errors", "ApproveShipments", h.GetTraceIDFromRequest(params.HTTPRequest), e.ValidationErrors) + return shipmentops.NewApproveShipmentsUnprocessableEntity().WithPayload(payload), err + case apperror.PreconditionFailedError: + return shipmentops.NewApproveShipmentsPreconditionFailed(). + WithPayload(&ghcmessages.Error{Message: handlers.FmtString(err.Error())}), err + case apperror.ConflictError, mtoshipment.ConflictStatusError: + return shipmentops.NewApproveShipmentsConflict().WithPayload(&ghcmessages.Error{Message: handlers.FmtString(err.Error())}), err + default: + return shipmentops.NewApproveShipmentsInternalServerError(). + WithPayload(&ghcmessages.Error{Message: handlers.FmtString("Internal server errors")}), err + } + } + + if len(params.Body.ApproveShipments) == 0 { + appCtx.Logger().Error("Invalid mto shipment: params Body is nil") + emptyBodyError := apperror.NewBadDataError("The MTO Shipment request body cannot be empty.") + payload := payloadForValidationError( + "Empty body error", + emptyBodyError.Error(), + h.GetTraceIDFromRequest(params.HTTPRequest), + validate.NewErrors(), + ) + + return shipmentops.NewApproveShipmentsUnprocessableEntity().WithPayload(payload), emptyBodyError + } + + var shipmentIdWithEtagArr []services.ShipmentIdWithEtag + for _, shipment := range params.Body.ApproveShipments { + shipmentID := uuid.FromStringOrNil(shipment.ShipmentID.String()) + + if shipment.ETag == nil { + return shipmentops.NewApproveShipmentsPreconditionFailed(). + WithPayload(&ghcmessages.Error{Message: handlers.FmtString("eTag is required for each shipment")}), nil + } + + shipmentIdWithEtagArr = append(shipmentIdWithEtagArr, services.ShipmentIdWithEtag{ + ShipmentID: shipmentID, + ETag: *shipment.ETag, + }) + } + + // Approve shipments + approvedShipments, err := h.ShipmentApprover.ApproveShipments(appCtx, shipmentIdWithEtagArr) + if err != nil { + appCtx.Logger().Error("Error approving shipments", zap.Error(err)) + return handleError(err) + } + + // Make the move available to prime + if approvedShipments != nil && len(*approvedShipments) > 0 { + move, wasMadeAvailableToPrime, err := h.MakeAvailableToPrime(appCtx, (*approvedShipments)[0].MoveTaskOrderID) + if err != nil { + appCtx.Logger().Error("Error making move available to prime", zap.Error(err)) + return handleError(err) + } + + // Execute tasks if the move has just become available to Prime (migrated from move_task_order.go) + if wasMadeAvailableToPrime { + /* Do not send TOO approving and submitting service items email if BLUEBARK/SAFETY */ + if move.Orders.CanSendEmailWithOrdersType() { + emailErr := h.NotificationSender().SendNotification(appCtx, + notifications.NewMoveIssuedToPrime(move.ID), + ) + if emailErr != nil { + return handleError(err) + } + } + + // Prepare move payload for auditing + moveTaskOrderPayload, err := payloads.Move(move, h.FileStorer()) + if err != nil { + return handleError(err) + } + // Audit attempt to make MTO available to prime + _, err = audit.Capture(appCtx, move, moveTaskOrderPayload, params.HTTPRequest) + if err != nil { + appCtx.Logger().Error("Auditing service error for making MTO available to Prime.", zap.Error(err)) + return handleError(err) + } + // Move update event + _, err = event.TriggerEvent(event.Event{ + EventKey: event.MoveTaskOrderUpdateEventKey, + MtoID: move.ID, + UpdatedObjectID: move.ID, + EndpointKey: event.GhcUpdateMoveTaskOrderStatusEndpointKey, + AppContext: appCtx, + TraceID: h.GetTraceIDFromRequest(params.HTTPRequest), + }) + if err != nil { + appCtx.Logger().Error("ghcapi.ApproveShipmentsHandlerFunc could not generate the event") + } + } + } + + // Prepare successful response payload + payload := make(ghcmessages.MTOShipments, len(*approvedShipments)) + for i, approvedShipment := range *approvedShipments { + h.triggerShipmentApprovalEvent(appCtx, approvedShipment.ID, approvedShipment.MoveTaskOrderID, params) + + shipmentSITStatus, _, err := h.CalculateShipmentSITStatus(appCtx, approvedShipment) + if err != nil { + return handleError(err) + } + + sitStatusPayload := payloads.SITStatus(shipmentSITStatus, h.FileStorer()) + payload[i] = payloads.MTOShipment(h.FileStorer(), &approvedShipment, sitStatusPayload) + } + + return shipmentops.NewApproveShipmentsOK().WithPayload(payload), nil + }) +} + +func (h ApproveShipmentsHandler) triggerShipmentApprovalEvent(appCtx appcontext.AppContext, shipmentID uuid.UUID, moveID uuid.UUID, params shipmentops.ApproveShipmentsParams) { + + _, err := event.TriggerEvent(event.Event{ + EndpointKey: event.GhcApproveShipmentsEndpointKey, + // Endpoint that is being handled + EventKey: event.ShipmentApproveEventKey, // Event that you want to trigger + UpdatedObjectID: shipmentID, // ID of the updated logical object + MtoID: moveID, // ID of the associated Move + AppContext: appCtx, + TraceID: h.GetTraceIDFromRequest(params.HTTPRequest), + }) + + // If the event trigger fails, just log the error. + if err != nil { + appCtx.Logger().Error("ghcapi.ApproveShipmentsHandler could not generate the event", zap.Error(err)) + } +} + // RequestShipmentDiversionHandler Requests a shipment diversion type RequestShipmentDiversionHandler struct { handlers.HandlerConfig diff --git a/pkg/handlers/ghcapi/mto_shipment_test.go b/pkg/handlers/ghcapi/mto_shipment_test.go index 67c79389e8c..70ac78cbaac 100644 --- a/pkg/handlers/ghcapi/mto_shipment_test.go +++ b/pkg/handlers/ghcapi/mto_shipment_test.go @@ -582,8 +582,62 @@ func (suite *HandlerSuite) TestGetShipmentHandler() { func (suite *HandlerSuite) TestApproveShipmentHandler() { waf := entitlements.NewWeightAllotmentFetcher() + setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { + mockCreator := &mocks.SignedCertificationCreator{} + + mockCreator.On( + "CreateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + ).Return(returnValue...) + + return mockCreator + } + + setUpSignedCertificationUpdaterMock := func(returnValue ...interface{}) services.SignedCertificationUpdater { + mockUpdater := &mocks.SignedCertificationUpdater{} + + mockUpdater.On( + "UpdateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + mock.AnythingOfType("string"), + ).Return(returnValue...) + + return mockUpdater + } + + builder := query.NewQueryBuilder() + moveRouter := moveservices.NewMoveRouter() + planner := &routemocks.Planner{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(400, nil) + ppmEstimator := mocks.PPMEstimator{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + ).Return(400, nil) + moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( + builder, + mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), &ppmEstimator, + ) + suite.Run("Returns 200 when all validations pass", func() { - move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + move := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + }, nil) shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ { Model: move, @@ -598,9 +652,6 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { eTag := etag.GenerateEtag(shipment.UpdatedAt) officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) - builder := query.NewQueryBuilder() - moveRouter := moveservices.NewMoveRouter() - planner := &routemocks.Planner{} moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester(), waf) planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), @@ -613,6 +664,8 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), &routemocks.Planner{}, moveWeights, + moveTaskOrderUpdater, + moveRouter, ) req := httptest.NewRequest("POST", fmt.Sprintf("/shipments/%s/approve", shipment.ID.String()), nil) @@ -628,6 +681,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ @@ -646,6 +700,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { suite.NoError(payload.Validate(strfmt.Default)) suite.HasWebhookNotification(shipment.ID, traceID) + suite.HasWebhookNotification(move.ID, traceID) // this action always creates a notification for the Prime }) suite.Run("Returns a 403 when the office user is not a TOO", func() { @@ -663,6 +718,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -702,6 +758,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -741,6 +798,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -780,6 +838,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -819,6 +878,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -858,6 +918,7 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { handlerConfig, approver, sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, } approveParams := shipmentops.ApproveShipmentParams{ HTTPRequest: req, @@ -876,6 +937,462 @@ func (suite *HandlerSuite) TestApproveShipmentHandler() { }) } +// ApproveShipment(s)Handler +func (suite *HandlerSuite) TestApproveShipmentsHandler() { + waf := entitlements.NewWeightAllotmentFetcher() + + setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { + mockCreator := &mocks.SignedCertificationCreator{} + + mockCreator.On( + "CreateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + ).Return(returnValue...) + + return mockCreator + } + + setUpSignedCertificationUpdaterMock := func(returnValue ...interface{}) services.SignedCertificationUpdater { + mockUpdater := &mocks.SignedCertificationUpdater{} + + mockUpdater.On( + "UpdateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + mock.AnythingOfType("string"), + ).Return(returnValue...) + + return mockUpdater + } + + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + builder := query.NewQueryBuilder() + moveRouter := moveservices.NewMoveRouter() + planner := &routemocks.Planner{} + moveWeights := moveservices.NewMoveWeights(mtoshipment.NewShipmentReweighRequester(), waf) + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + false, + ).Return(400, nil) + + ppmEstimator := mocks.PPMEstimator{} + planner.On("ZipTransitDistance", + mock.AnythingOfType("*appcontext.appContext"), + mock.Anything, + mock.Anything, + false, + ).Return(400, nil) + moveTaskOrderUpdater := movetaskorder.NewMoveTaskOrderUpdater( + builder, + mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), &ppmEstimator, + ) + + suite.Run("Returns 200 when all validations pass", func() { + move := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + }, nil) + shipment1 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + shipment2 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + eTag1 := etag.GenerateEtag(shipment1.UpdatedAt) + eTag2 := etag.GenerateEtag(shipment2.UpdatedAt) + + approver := mtoshipment.NewShipmentApprover( + mtoshipment.NewShipmentRouter(), + mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + &routemocks.Planner{}, + moveWeights, + moveTaskOrderUpdater, + moveRouter, + ) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + + traceID, err := uuid.NewV4() + suite.FatalNoError(err, "Error creating a new trace ID.") + req = req.WithContext(trace.NewContext(req.Context(), traceID)) + + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment1.ID), + ETag: &eTag1, + }, + { + ShipmentID: handlers.FmtUUID(shipment2.ID), + ETag: &eTag2, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsOK{}, response) + payload := response.(*shipmentops.ApproveShipmentsOK).Payload + + // Validate outgoing payload + suite.NoError(payload[0].Validate(strfmt.Default)) + suite.NoError(payload[1].Validate(strfmt.Default)) + + suite.HasWebhookNotification(move.ID, traceID) // this action always creates a notification for the Prime + suite.HasWebhookNotification(shipment1.ID, traceID) + suite.HasWebhookNotification(shipment2.ID, traceID) + }) + + suite.Run("Returns a 403 when the office user is not a TOO", func() { + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + eTag := etag.GenerateEtag(shipment.UpdatedAt) + + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeServicesCounselor}) + approver := &mocks.ShipmentApprover{} + + approver.AssertNumberOfCalls(suite.T(), "ApproveShipments", 0) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsForbidden{}, response) + payload := response.(*shipmentops.ApproveShipmentsForbidden).Payload + + // Validate outgoing payload: nil payload + suite.Nil(payload) + }) + + suite.Run("Returns 404 when approver returns NotFoundError", func() { + shipment := factory.BuildMTOShipmentMinimal(nil, []factory.Customization{ + { + Model: models.MTOShipment{ + ID: uuid.Must(uuid.NewV4()), + }, + }, + }, nil) + eTag := etag.GenerateEtag(shipment.UpdatedAt) + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, apperror.NotFoundError{}) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsNotFound{}, response) + payload := response.(*shipmentops.ApproveShipmentsNotFound).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) + + suite.Run("Returns 409 when approver returns Conflict Error", func() { + shipment := factory.BuildMTOShipmentMinimal(nil, []factory.Customization{ + { + Model: models.MTOShipment{ + ID: uuid.Must(uuid.NewV4()), + }, + }, + }, nil) + eTag := etag.GenerateEtag(shipment.UpdatedAt) + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, apperror.ConflictError{}) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsConflict{}, response) + payload := response.(*shipmentops.ApproveShipmentsConflict).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) + + suite.Run("Returns 412 when eTag does not match", func() { + shipment := factory.BuildMTOShipmentMinimal(nil, []factory.Customization{ + { + Model: models.MTOShipment{ + ID: uuid.Must(uuid.NewV4()), + }, + }, + }, nil) + eTag := etag.GenerateEtag(time.Now()) + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, apperror.PreconditionFailedError{}) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsPreconditionFailed{}, response) + payload := response.(*shipmentops.ApproveShipmentsPreconditionFailed).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) + + suite.Run("Returns 422 when approver returns validation errors", func() { + shipment := factory.BuildMTOShipmentMinimal(nil, []factory.Customization{ + { + Model: models.MTOShipment{ + ID: uuid.Must(uuid.NewV4()), + }, + }, + }, nil) + eTag := etag.GenerateEtag(shipment.UpdatedAt) + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, apperror.InvalidInputError{ValidationErrors: &validate.Errors{}}) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsUnprocessableEntity{}, response) + payload := response.(*shipmentops.ApproveShipmentsUnprocessableEntity).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) + + suite.Run("Returns 422 when ApproveShipments body is empty", func() { + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, apperror.InvalidInputError{ValidationErrors: &validate.Errors{}}) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{}, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsUnprocessableEntity{}, response) + payload := response.(*shipmentops.ApproveShipmentsUnprocessableEntity).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) + + suite.Run("Returns 500 when approver returns unexpected error", func() { + shipment := factory.BuildMTOShipmentMinimal(nil, []factory.Customization{ + { + Model: models.MTOShipment{ + ID: uuid.Must(uuid.NewV4()), + }, + }, + }, nil) + eTag := etag.GenerateEtag(shipment.UpdatedAt) + officeUser := factory.BuildOfficeUserWithRoles(nil, nil, []roles.RoleType{roles.RoleTypeTOO}) + approver := &mocks.ShipmentApprover{} + + approver.On("ApproveShipments", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("[]services.ShipmentIdWithEtag")).Return(nil, errors.New("UnexpectedError")) + + req := httptest.NewRequest("POST", "/shipments/approve", nil) + req = suite.AuthenticateOfficeRequest(req, officeUser) + handlerConfig := suite.HandlerConfig() + + handler := ApproveShipmentsHandler{ + handlerConfig, + approver, + sitstatus.NewShipmentSITStatus(), + moveTaskOrderUpdater, + } + approveParams := shipmentops.ApproveShipmentsParams{ + HTTPRequest: req, + Body: &ghcmessages.ApproveShipments{ + ApproveShipments: []*ghcmessages.ApproveShipmentsApproveShipmentsItems0{ + { + ShipmentID: handlers.FmtUUID(shipment.ID), + ETag: &eTag, + }, + }, + }, + } + + // Validate incoming payload: no body to validate + response := handler.Handle(approveParams) + suite.IsType(&shipmentops.ApproveShipmentsInternalServerError{}, response) + payload := response.(*shipmentops.ApproveShipmentsInternalServerError).Payload + + // Validate outgoing payload + suite.NoError(payload.Validate(strfmt.Default)) + }) +} + func (suite *HandlerSuite) TestRequestShipmentDiversionHandler() { diversionReason := "Test Reason" @@ -4313,9 +4830,7 @@ func (suite *HandlerSuite) TestUpdateShipmentHandler() { suite.Equal(handlers.FmtPoundPtr(&sitEstimatedWeight), updatedShipment.PpmShipment.SitEstimatedWeight) suite.Equal(handlers.FmtDate(sitEstimatedEntryDate), updatedShipment.PpmShipment.SitEstimatedEntryDate) suite.Equal(handlers.FmtDate(sitEstimatedDepartureDate), updatedShipment.PpmShipment.SitEstimatedDepartureDate) - suite.Equal(int64(sitEstimatedCost), *updatedShipment.PpmShipment.SitEstimatedCost) suite.Equal(handlers.FmtPoundPtr(&estimatedWeight), updatedShipment.PpmShipment.EstimatedWeight) - suite.Equal(int64(estimatedIncentive), *updatedShipment.PpmShipment.EstimatedIncentive) suite.Equal(handlers.FmtBool(hasProGear), updatedShipment.PpmShipment.HasProGear) suite.Equal(handlers.FmtPoundPtr(&proGearWeight), updatedShipment.PpmShipment.ProGearWeight) suite.Equal(handlers.FmtPoundPtr(&spouseProGearWeight), updatedShipment.PpmShipment.SpouseProGearWeight) diff --git a/pkg/handlers/ghcapi/orders.go b/pkg/handlers/ghcapi/orders.go index 51fec571935..8a8ca3cafcf 100644 --- a/pkg/handlers/ghcapi/orders.go +++ b/pkg/handlers/ghcapi/orders.go @@ -290,6 +290,8 @@ func (h CreateOrderHandler) Handle(params orderop.CreateOrderParams) middleware. weightAllotment.UnaccompaniedBaggageAllowance = unaccompaniedBaggageAllowance } + var weightRestriction *int + entitlement := models.Entitlement{ DependentsAuthorized: payload.HasDependents, DBAuthorizedWeight: models.IntPointer(weight), @@ -300,6 +302,7 @@ func (h CreateOrderHandler) Handle(params orderop.CreateOrderParams) middleware. DependentsUnderTwelve: dependentsUnderTwelve, DependentsTwelveAndOver: dependentsTwelveAndOver, UBAllowance: &weightAllotment.UnaccompaniedBaggageAllowance, + WeightRestriction: weightRestriction, } if saveEntitlementErr := appCtx.DB().Save(&entitlement); saveEntitlementErr != nil { @@ -362,15 +365,13 @@ func (h CreateOrderHandler) Handle(params orderop.CreateOrderParams) middleware. Show: models.BoolPointer(true), Status: &status, } - if !appCtx.Session().OfficeUserID.IsNil() { - officeUser, err := models.FetchOfficeUserByID(appCtx.DB(), appCtx.Session().OfficeUserID) + + if payload.CounselingOfficeID != nil { + counselingOffice, err := uuid.FromString(payload.CounselingOfficeID.String()) if err != nil { - err = apperror.NewBadDataError("Unable to fetch office user.") - appCtx.Logger().Error(err.Error()) - return orderop.NewCreateOrderUnprocessableEntity(), err - } else { - moveOptions.CounselingOfficeID = &officeUser.TransportationOfficeID + return handlers.ResponseForError(appCtx.Logger(), err), err } + moveOptions.CounselingOfficeID = &counselingOffice } if newOrder.OrdersType == "SAFETY" { diff --git a/pkg/handlers/ghcapi/orders_test.go b/pkg/handlers/ghcapi/orders_test.go index 68b984f2cef..4496c7c1146 100644 --- a/pkg/handlers/ghcapi/orders_test.go +++ b/pkg/handlers/ghcapi/orders_test.go @@ -76,6 +76,7 @@ func (suite *HandlerSuite) TestCreateOrder() { Sac: handlers.FmtString("SacNumber"), DepartmentIndicator: ghcmessages.NewDeptIndicator(deptIndicator), Grade: ghcmessages.GradeE1.Pointer(), + CounselingOfficeID: handlers.FmtUUID(*dutyLocation.TransportationOfficeID), } params := orderop.CreateOrderParams{ @@ -1531,6 +1532,7 @@ func (suite *HandlerSuite) TestCounselingUpdateAllowanceHandler() { ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, StorageInTransit: models.Int64Pointer(80), + WeightRestriction: models.Int64Pointer(0), } request := httptest.NewRequest("PATCH", "/counseling/orders/{orderID}/allowances", nil) diff --git a/pkg/handlers/ghcapi/queues.go b/pkg/handlers/ghcapi/queues.go index f4b124cbb1c..88c040e4c16 100644 --- a/pkg/handlers/ghcapi/queues.go +++ b/pkg/handlers/ghcapi/queues.go @@ -64,6 +64,11 @@ func (h GetMovesQueueHandler) Handle(params queues.GetMovesQueueParams) middlewa CounselingOffice: params.CounselingOffice, } + var activeRole string + if params.ActiveRole != nil { + activeRole = *params.ActiveRole + } + // When no status filter applied, TOO should only see moves with status of New Move, Service Counseling Completed, or Approvals Requested if params.Status == nil { ListOrderParams.Status = []string{string(models.MoveStatusServiceCounselingCompleted), string(models.MoveStatusAPPROVALSREQUESTED), string(models.MoveStatusSUBMITTED)} @@ -165,7 +170,7 @@ func (h GetMovesQueueHandler) Handle(params queues.GetMovesQueueParams) middlewa } } - queueMoves := payloads.QueueMoves(moves, officeUsers, nil, officeUser, officeUsersSafety) + queueMoves := payloads.QueueMoves(moves, officeUsers, nil, officeUser, officeUsersSafety, activeRole) result := &ghcmessages.QueueMovesResult{ Page: *ListOrderParams.Page, @@ -391,6 +396,11 @@ func (h GetPaymentRequestsQueueHandler) Handle( CounselingOffice: params.CounselingOffice, } + var activeRole string + if params.ActiveRole != nil { + activeRole = *params.ActiveRole + } + listPaymentRequestParams.Status = []string{string(models.QueuePaymentRequestPaymentRequested)} // Let's set default values for page and perPage if we don't get arguments for them. We'll use 1 for page and 20 @@ -488,7 +498,7 @@ func (h GetPaymentRequestsQueueHandler) Handle( } } - queuePaymentRequests := payloads.QueuePaymentRequests(paymentRequests, officeUsers, officeUser, officeUsersSafety) + queuePaymentRequests := payloads.QueuePaymentRequests(paymentRequests, officeUsers, officeUser, officeUsersSafety, activeRole) result := &ghcmessages.QueuePaymentRequestsResult{ TotalCount: int64(count), @@ -549,6 +559,11 @@ func (h GetServicesCounselingQueueHandler) Handle( SCAssignedUser: params.AssignedTo, } + var activeRole string + if params.ActiveRole != nil { + activeRole = *params.ActiveRole + } + var requestedPpmStatus models.PPMShipmentStatus if params.NeedsPPMCloseout != nil && *params.NeedsPPMCloseout { requestedPpmStatus = models.PPMShipmentStatusNeedsCloseout @@ -658,7 +673,7 @@ func (h GetServicesCounselingQueueHandler) Handle( } } - queueMoves := payloads.QueueMoves(moves, officeUsers, &requestedPpmStatus, officeUser, officeUsersSafety) + queueMoves := payloads.QueueMoves(moves, officeUsers, &requestedPpmStatus, officeUser, officeUsersSafety, activeRole) result := &ghcmessages.QueueMovesResult{ Page: *ListOrderParams.Page, @@ -773,6 +788,27 @@ func (h GetBulkAssignmentDataHandler) Handle( return queues.NewGetBulkAssignmentDataInternalServerError(), err } + officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) + case string(models.QueueTypePaymentRequest): + // fetch the TIOs who work at their office + officeUsers, err := h.OfficeUserFetcherPop.FetchOfficeUsersWithWorkloadByRoleAndOffice( + appCtx, + roles.RoleTypeTIO, + officeUser.TransportationOfficeID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving office users", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + // fetch the moves available to be assigned to their office users + moves, err := h.MoveFetcherBulkAssignment.FetchMovesForBulkAssignmentPaymentRequest( + appCtx, officeUser.TransportationOffice.Gbloc, officeUser.TransportationOffice.ID, + ) + if err != nil { + appCtx.Logger().Error("Error retreiving moves", zap.Error(err)) + return queues.NewGetBulkAssignmentDataInternalServerError(), err + } + officeUserData = payloads.BulkAssignmentData(appCtx, moves, officeUsers, officeUser.TransportationOffice.ID) } return queues.NewGetBulkAssignmentDataOK().WithPayload(&officeUserData), nil diff --git a/pkg/handlers/ghcapi/queues_test.go b/pkg/handlers/ghcapi/queues_test.go index 6de9b13dd91..90088cf2ec0 100644 --- a/pkg/handlers/ghcapi/queues_test.go +++ b/pkg/handlers/ghcapi/queues_test.go @@ -1655,7 +1655,7 @@ func (suite *HandlerSuite) TestGetServicesCounselingQueueHandler() { } func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { - suite.Run("returns an unauthorized error when an attempt is made by a non supervisor", func() { + suite.Run("SC - returns an unauthorized error when an attempt is made by a non supervisor", func() { officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ { Model: models.OfficeUser{ @@ -1689,7 +1689,7 @@ func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { suite.IsNotErrResponse(response) suite.IsType(&queues.GetBulkAssignmentDataUnauthorized{}, response) }) - suite.Run("returns properly formatted bulk assignment data", func() { + suite.Run("SC - returns properly formatted bulk assignment data", func() { transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ @@ -1892,6 +1892,362 @@ func (suite *HandlerSuite) TestGetBulkAssignmentDataHandler() { suite.Len(payload.AvailableOfficeUsers, 1) suite.Len(payload.BulkAssignmentMoveIDs, 1) }) + + suite.Run("TIO - returns an unauthorized error when an attempt is made by a non supervisor", func() { + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + }, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTIO, + }, + }, + }, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/bulk-assignment", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.GetBulkAssignmentDataParams{ + HTTPRequest: request, + QueueType: models.StringPointer("PAYMENT_REQUEST"), + } + handlerConfig := suite.HandlerConfig() + handler := GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcherBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetBulkAssignmentDataUnauthorized{}, response) + }) + suite.Run("TIO - returns properly formatted bulk assignment data", func() { + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: roles.RoleTypeTIO, + }, + }, + }, + }, + }, nil) + + // payment request to appear in the return + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/bulk-assignment", nil) + request = suite.AuthenticateOfficeRequest(request, officeUser) + params := queues.GetBulkAssignmentDataParams{ + HTTPRequest: request, + QueueType: models.StringPointer("PAYMENT_REQUEST"), + } + handlerConfig := suite.HandlerConfig() + handler := GetBulkAssignmentDataHandler{ + handlerConfig, + officeusercreator.NewOfficeUserFetcherPop(), + movefetcher.NewMoveFetcherBulkAssignment(), + } + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetBulkAssignmentDataOK{}, response) + payload := response.(*queues.GetBulkAssignmentDataOK).Payload + suite.NoError(payload.Validate(strfmt.Default)) + suite.Len(payload.AvailableOfficeUsers, 1) + suite.Len(payload.BulkAssignmentMoveIDs, 1) + }) +} + +type availableOfficeUserSubtestData struct { + officeUsers []models.OfficeUser + office models.TransportationOffice +} + +func (suite *HandlerSuite) TestAvailableOfficeUsers() { + setupOfficeUserData := func(role1 roles.RoleType, role2 roles.RoleType) availableOfficeUserSubtestData { + subtestData := &availableOfficeUserSubtestData{} + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + // lets generate a few office users + // these first two are what we want returned in the query + // office user 1 is the supervisor making the request + officeUser1 := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + LastName: "Aname", + Email: "officeuser1@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Privileges: []models.Privilege{ + { + PrivilegeType: models.PrivilegeTypeSupervisor, + }, + }, + Roles: []roles.Role{ + { + RoleType: role1, + }, + }, + }, + }, + }, nil) + + // officeUser2 is their underling + officeUser2 := factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + LastName: "Bname", + Email: "officeuser2@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: role1, + }, + }, + }, + }, + }, nil) + + // this office user shares their role but does NOT work at their office so should not be returned + factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser3@example.com", + Active: true, + }, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: role1, + }, + }, + }, + }, + }, nil) + + // this office users works at their office, but doesn't share the same role, and should not be returned + factory.BuildOfficeUserWithPrivileges(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Email: "officeuser4@example.com", + Active: true, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.User{ + Roles: []roles.Role{ + { + RoleType: role2, + }, + }, + }, + }, + }, nil) + + availableOfficeUsers := []models.OfficeUser{officeUser1, officeUser2} + subtestData.officeUsers = availableOfficeUsers + subtestData.office = transportationOffice + return *subtestData + } + suite.Run("properly fetches a TOO supervisor's available office users for assignment", func() { + subtestData := setupOfficeUserData(roles.RoleTypeTOO, roles.RoleTypeServicesCounselor) + waf := entitlements.NewWeightAllotmentFetcher() + + hhgMove := factory.BuildSubmittedMove(suite.DB(), nil, nil) + + factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: hhgMove, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/moves", nil) + request = suite.AuthenticateOfficeRequest(request, subtestData.officeUsers[0]) + params := queues.GetMovesQueueParams{ + HTTPRequest: request, + } + handlerConfig := suite.HandlerConfig() + mockUnlocker := movelocker.NewMoveUnlocker() + handler := GetMovesQueueHandler{ + handlerConfig, + order.NewOrderFetcher(waf), + mockUnlocker, + officeusercreator.NewOfficeUserFetcherPop(), + } + + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetMovesQueueOK{}, response) + payload := response.(*queues.GetMovesQueueOK).Payload + + suite.NotNil(payload.QueueMoves) + suite.NotNil(payload.QueueMoves[0].AvailableOfficeUsers) + suite.Equal(2, len(payload.QueueMoves[0].AvailableOfficeUsers)) + suite.Equal(subtestData.officeUsers[0].ID.String(), payload.QueueMoves[0].AvailableOfficeUsers[0].OfficeUserID.String()) + suite.Equal(subtestData.officeUsers[1].ID.String(), payload.QueueMoves[0].AvailableOfficeUsers[1].OfficeUserID.String()) + }) + suite.Run("properly fetches a SC supervisor's available office users for assignment", func() { + subtestData := setupOfficeUserData(roles.RoleTypeServicesCounselor, roles.RoleTypeTOO) + waf := entitlements.NewWeightAllotmentFetcher() + + needsCounselingMove := factory.BuildNeedsServiceCounselingMove(suite.DB(), []factory.Customization{ + { + Model: subtestData.office, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: needsCounselingMove, + LinkOnly: true, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/counseling", nil) + request = suite.AuthenticateOfficeRequest(request, subtestData.officeUsers[0]) + params := queues.GetServicesCounselingQueueParams{ + HTTPRequest: request, + } + handlerConfig := suite.HandlerConfig() + mockUnlocker := movelocker.NewMoveUnlocker() + handler := GetServicesCounselingQueueHandler{ + handlerConfig, + order.NewOrderFetcher(waf), + mockUnlocker, + officeusercreator.NewOfficeUserFetcherPop(), + } + + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetServicesCounselingQueueOK{}, response) + payload := response.(*queues.GetServicesCounselingQueueOK).Payload + + suite.NotNil(payload.QueueMoves) + suite.NotNil(payload.QueueMoves[0].AvailableOfficeUsers) + suite.Equal(2, len(payload.QueueMoves[0].AvailableOfficeUsers)) + suite.Equal(subtestData.officeUsers[0].ID.String(), payload.QueueMoves[0].AvailableOfficeUsers[0].OfficeUserID.String()) + suite.Equal(subtestData.officeUsers[1].ID.String(), payload.QueueMoves[0].AvailableOfficeUsers[1].OfficeUserID.String()) + }) + + suite.Run("properly fetches a TIO supervisor's available office users for assignment", func() { + subtestData := setupOfficeUserData(roles.RoleTypeTIO, roles.RoleTypeTOO) + hhgMove := factory.BuildMoveWithShipment(suite.DB(), nil, nil) + + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: hhgMove, + LinkOnly: true, + }, + }, nil) + + request := httptest.NewRequest("GET", "/queues/payment-requests", nil) + request = suite.AuthenticateOfficeRequest(request, subtestData.officeUsers[0]) + params := queues.GetPaymentRequestsQueueParams{ + HTTPRequest: request, + } + handlerConfig := suite.HandlerConfig() + mockUnlocker := movelocker.NewMoveUnlocker() + handler := GetPaymentRequestsQueueHandler{ + handlerConfig, + paymentrequest.NewPaymentRequestListFetcher(), + mockUnlocker, + officeusercreator.NewOfficeUserFetcherPop(), + } + + response := handler.Handle(params) + suite.IsNotErrResponse(response) + suite.IsType(&queues.GetPaymentRequestsQueueOK{}, response) + payload := response.(*queues.GetPaymentRequestsQueueOK).Payload + + suite.NotNil(payload.QueuePaymentRequests) + suite.NotNil(payload.QueuePaymentRequests[0].AvailableOfficeUsers) + suite.Equal(2, len(payload.QueuePaymentRequests[0].AvailableOfficeUsers)) + suite.Equal(subtestData.officeUsers[0].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[0].OfficeUserID.String()) + suite.Equal(subtestData.officeUsers[1].ID.String(), payload.QueuePaymentRequests[0].AvailableOfficeUsers[1].OfficeUserID.String()) + }) + } func (suite *HandlerSuite) TestGetDestinationRequestsQueuesHandler() { diff --git a/pkg/handlers/ghcapi/tranportation_offices.go b/pkg/handlers/ghcapi/tranportation_offices.go index 405580923bb..b08f45cfb04 100644 --- a/pkg/handlers/ghcapi/tranportation_offices.go +++ b/pkg/handlers/ghcapi/tranportation_offices.go @@ -2,6 +2,7 @@ package ghcapi import ( "github.com/go-openapi/runtime/middleware" + "github.com/gofrs/uuid" "go.uber.org/zap" "github.com/transcom/mymove/pkg/appcontext" @@ -22,7 +23,7 @@ func (h GetTransportationOfficesHandler) Handle(params transportationofficeop.Ge // B-21022: forPpm param is set true. This is used by PPM closeout widget. Need to ensure certain offices are included/excluded // if location has ppm closedout enabled. - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) @@ -43,7 +44,7 @@ func (h GetTransportationOfficesOpenHandler) Handle(params transportationofficeo return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, func(appCtx appcontext.AppContext) (middleware.Responder, error) { - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, false) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, false, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) return transportationofficeop.NewGetTransportationOfficesOpenInternalServerError(), err @@ -73,3 +74,27 @@ func (h GetTransportationOfficesGBLOCsHandler) Handle(params transportationoffic return transportationofficeop.NewGetTransportationOfficesGBLOCsOK().WithPayload(returnPayload), nil }) } + +// ShowCounselingOfficesHandler returns the counseling offices for a duty location ID +type ShowCounselingOfficesHandler struct { + handlers.HandlerConfig + services.TransportationOfficesFetcher +} + +// Handle retrieves the counseling offices in the system for a given duty location ID +func (h ShowCounselingOfficesHandler) Handle(params transportationofficeop.ShowCounselingOfficesParams) middleware.Responder { + return h.AuditableAppContextFromRequestWithErrors(params.HTTPRequest, + func(appCtx appcontext.AppContext) (middleware.Responder, error) { + dutyLocationID := uuid.FromStringOrNil(params.DutyLocationID.String()) + serviceMemberID := uuid.FromStringOrNil(params.ServiceMemberID.String()) + + counselingOffices, err := h.TransportationOfficesFetcher.GetCounselingOffices(appCtx, dutyLocationID, serviceMemberID) + if err != nil { + appCtx.Logger().Error("Error searching for Counseling Offices: ", zap.Error(err)) + return transportationofficeop.NewShowCounselingOfficesInternalServerError(), err + } + + returnPayload := payloads.CounselingOffices(*counselingOffices) + return transportationofficeop.NewShowCounselingOfficesOK().WithPayload(returnPayload), nil + }) +} diff --git a/pkg/handlers/ghcapi/transportation_offices_test.go b/pkg/handlers/ghcapi/transportation_offices_test.go index 92ac98f630b..b9305f4d4b7 100644 --- a/pkg/handlers/ghcapi/transportation_offices_test.go +++ b/pkg/handlers/ghcapi/transportation_offices_test.go @@ -1,13 +1,16 @@ package ghcapi import ( + "fmt" "net/http/httptest" "github.com/go-openapi/strfmt" "github.com/transcom/mymove/pkg/factory" transportationofficeop "github.com/transcom/mymove/pkg/gen/ghcapi/ghcoperations/transportation_office" + "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services/address" transportationofficeservice "github.com/transcom/mymove/pkg/services/transportation_office" ) @@ -145,3 +148,63 @@ func (suite *HandlerSuite) TestGetTransportationOfficesGBLOCsHandler() { suite.Equal(transportationOffice1.Gbloc, responsePayload.Payload[0]) suite.Equal(transportationOffice2.Gbloc, responsePayload.Payload[1]) } + +func (suite *HandlerSuite) TestShowCounselingOfficesHandler() { + user := factory.BuildDefaultUser(suite.DB()) + serviceMember := factory.BuildServiceMember(suite.DB(), nil, nil) + + fetcher := transportationofficeservice.NewTransportationOfficesFetcher() + + newAddress := models.Address{ + StreetAddress1: "some address", + City: "city", + State: "CA", + PostalCode: "59801", + County: models.StringPointer("County"), + } + addressCreator := address.NewAddressCreator() + createdAddress, err := addressCreator.CreateAddress(suite.AppContextForTest(), &newAddress) + suite.NoError(err) + + origDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + AddressID: createdAddress.ID, + ProvidesServicesCounseling: true, + }, + }, + { + Model: models.TransportationOffice{ + Name: "New PPPO Travis AFB - USAF", + Gbloc: "KKFA", + ProvidesCloseout: true, + }, + }, + }, nil) + + path := fmt.Sprintf("/transportation_offices/%v/counseling_offices/%v", origDutyLocation.ID.String(), serviceMember.ID.String()) + req := httptest.NewRequest("GET", path, nil) + req = suite.AuthenticateUserRequest(req, user) + params := transportationofficeop.ShowCounselingOfficesParams{ + HTTPRequest: req, + DutyLocationID: *handlers.FmtUUID(origDutyLocation.ID), + } + + handler := ShowCounselingOfficesHandler{ + HandlerConfig: suite.HandlerConfig(), + TransportationOfficesFetcher: fetcher} + + response := handler.Handle(params) + suite.Assertions.IsType(&transportationofficeop.ShowCounselingOfficesOK{}, response) + responsePayload := response.(*transportationofficeop.ShowCounselingOfficesOK) + + // Validate outgoing payload + suite.NoError(responsePayload.Payload.Validate(strfmt.Default)) + var i int + for index, office := range responsePayload.Payload { + if *office.Name == "New PPPO Travis AFB - USAF" { + i = index + } + } + suite.NotNil(i) +} diff --git a/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go index 80f07a55433..3fa132f085a 100644 --- a/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/internalapi/internal/payloads/model_to_payload_test.go @@ -6,6 +6,7 @@ import ( "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/etag" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/gen/internalmessages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" @@ -183,3 +184,34 @@ func (suite *PayloadsSuite) TestWeightTicket() { suite.Equal(etag.GenerateEtag(weightTicket.UpdatedAt), parsedWeightTicket.ETag) }) } + +func (suite *PayloadsSuite) TestCounselingOffices() { + suite.Run("correctly maps transportaion offices to counseling offices payload", func() { + office1 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Liberty", + }, + }, + }, nil) + + office2 := factory.BuildTransportationOffice(nil, []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + Name: "PPPO Fort Walker", + }, + }, + }, nil) + + offices := models.TransportationOffices{office1, office2} + + payload := CounselingOffices(offices) + + suite.IsType(payload, internalmessages.CounselingOffices{}) + suite.Equal(2, len(payload)) + suite.Equal(office1.ID.String(), payload[0].ID.String()) + suite.Equal(office2.ID.String(), payload[1].ID.String()) + }) +} diff --git a/pkg/handlers/internalapi/orders.go b/pkg/handlers/internalapi/orders.go index 84c8dfe126f..0097e94b7e3 100644 --- a/pkg/handlers/internalapi/orders.go +++ b/pkg/handlers/internalapi/orders.go @@ -85,6 +85,9 @@ func payloadForOrdersModel(storer storage.FileStorer, order models.Order) (*inte if order.Entitlement.UBAllowance != nil { entitlement.UbAllowance = models.Int64Pointer(int64(*order.Entitlement.UBAllowance)) } + if order.Entitlement.WeightRestriction != nil { + entitlement.WeightRestriction = models.Int64Pointer(int64(*order.Entitlement.WeightRestriction)) + } } var originDutyLocation models.DutyLocation originDutyLocation = models.DutyLocation{} diff --git a/pkg/handlers/internalapi/transportation_offices.go b/pkg/handlers/internalapi/transportation_offices.go index 6b535fd9ba9..7f8529fe479 100644 --- a/pkg/handlers/internalapi/transportation_offices.go +++ b/pkg/handlers/internalapi/transportation_offices.go @@ -51,7 +51,7 @@ func (h GetTransportationOfficesHandler) Handle(params transportationofficeop.Ge return transportationofficeop.NewGetTransportationOfficesForbidden(), noServiceMemberIDErr } - transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true) + transportationOffices, err := h.TransportationOfficesFetcher.GetTransportationOffices(appCtx, params.Search, true, false) if err != nil { appCtx.Logger().Error("Error searching for Transportation Offices: ", zap.Error(err)) return transportationofficeop.NewGetTransportationOfficesInternalServerError(), err @@ -74,7 +74,7 @@ func (h ShowCounselingOfficesHandler) Handle(params transportationofficeop.ShowC func(appCtx appcontext.AppContext) (middleware.Responder, error) { dutyLocationID := uuid.FromStringOrNil(params.DutyLocationID.String()) - counselingOffices, err := h.TransportationOfficesFetcher.GetCounselingOffices(appCtx, dutyLocationID) + counselingOffices, err := h.TransportationOfficesFetcher.GetCounselingOffices(appCtx, dutyLocationID, appCtx.Session().ServiceMemberID) if err != nil { appCtx.Logger().Error("Error searching for Counseling Offices: ", zap.Error(err)) return transportationofficeop.NewShowCounselingOfficesInternalServerError(), err diff --git a/pkg/handlers/internalapi/transportation_offices_test.go b/pkg/handlers/internalapi/transportation_offices_test.go index b41c5aff7bf..64cd3db6cb2 100644 --- a/pkg/handlers/internalapi/transportation_offices_test.go +++ b/pkg/handlers/internalapi/transportation_offices_test.go @@ -153,7 +153,6 @@ func (suite *HandlerSuite) TestShowCounselingOfficesHandler() { }, }, }, nil) - suite.MustSave(&origDutyLocation) path := fmt.Sprintf("/transportation_offices/%v/counseling_offices", origDutyLocation.ID.String()) req := httptest.NewRequest("GET", path, nil) @@ -174,4 +173,13 @@ func (suite *HandlerSuite) TestShowCounselingOfficesHandler() { // Validate outgoing payload suite.NoError(responsePayload.Payload.Validate(strfmt.Default)) + // Validate outgoing payload + suite.NoError(responsePayload.Payload.Validate(strfmt.Default)) + var i int + for index, office := range responsePayload.Payload { + if *office.Name == "New PPPO Travis AFB - USAF" { + i = index + } + } + suite.NotNil(i) } diff --git a/pkg/handlers/primeapi/move_task_order_test.go b/pkg/handlers/primeapi/move_task_order_test.go index cfbc270140d..f22e72e5e41 100644 --- a/pkg/handlers/primeapi/move_task_order_test.go +++ b/pkg/handlers/primeapi/move_task_order_test.go @@ -1329,7 +1329,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.NotNil(payload.ETag()) }) - suite.Run("Success - return all MTOServiceItemShuttle fields assoicated with the getMoveTaskOrder", func() { + suite.Run("Success - return all MTOServiceItemDomesticShuttle fields assoicated with the getMoveTaskOrder", func() { handler := GetMoveTaskOrderHandler{ suite.HandlerConfig(), movetaskorder.NewMoveTaskOrderFetcher(waf), @@ -1396,14 +1396,14 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { json, err := json.Marshal(serviceItemPayload) suite.NoError(err) - payload := primemessages.MTOServiceItemShuttle{} + payload := primemessages.MTOServiceItemDomesticShuttle{} err = payload.UnmarshalJSON(json) suite.NoError(err) suite.Equal(serviceItem.MoveTaskOrderID.String(), payload.MoveTaskOrderID().String()) suite.Equal(serviceItem.MTOShipmentID.String(), payload.MtoShipmentID().String()) suite.Equal(serviceItem.ID.String(), payload.ID().String()) - suite.Equal("MTOServiceItemShuttle", string(payload.ModelType())) + suite.Equal("MTOServiceItemDomesticShuttle", string(payload.ModelType())) suite.Equal(string(serviceItem.ReService.Code), string(*payload.ReServiceCode)) suite.Equal(serviceItem.ReService.Name, payload.ReServiceName()) suite.Equal(string(serviceItem.Status), string(payload.Status())) diff --git a/pkg/handlers/primeapi/mto_service_item.go b/pkg/handlers/primeapi/mto_service_item.go index 646c6d17bfb..c78f37b6950 100644 --- a/pkg/handlers/primeapi/mto_service_item.go +++ b/pkg/handlers/primeapi/mto_service_item.go @@ -28,6 +28,7 @@ var CreateableServiceItemMap = map[primemessages.MTOServiceItemModelType]bool{ primemessages.MTOServiceItemModelTypeMTOServiceItemOriginSIT: true, primemessages.MTOServiceItemModelTypeMTOServiceItemDestSIT: true, primemessages.MTOServiceItemModelTypeMTOServiceItemShuttle: true, + primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: true, primemessages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: true, primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticCrating: true, primemessages.MTOServiceItemModelTypeMTOServiceItemInternationalCrating: true, diff --git a/pkg/handlers/primeapi/payloads/model_to_payload.go b/pkg/handlers/primeapi/payloads/model_to_payload.go index af39f03827f..5a675099271 100644 --- a/pkg/handlers/primeapi/payloads/model_to_payload.go +++ b/pkg/handlers/primeapi/payloads/model_to_payload.go @@ -262,6 +262,10 @@ func Entitlement(entitlement *models.Entitlement) *primemessages.Entitlements { if entitlement.UBAllowance != nil { ubAllowance = int64(*entitlement.UBAllowance) } + var weightRestriction int64 + if entitlement.WeightRestriction != nil { + weightRestriction = int64(*entitlement.WeightRestriction) + } return &primemessages.Entitlements{ ID: strfmt.UUID(entitlement.ID.String()), AuthorizedWeight: authorizedWeight, @@ -274,10 +278,11 @@ func Entitlement(entitlement *models.Entitlement) *primemessages.Entitlements { ProGearWeightSpouse: int64(entitlement.ProGearWeightSpouse), RequiredMedicalEquipmentWeight: int64(entitlement.RequiredMedicalEquipmentWeight), OrganizationalClothingAndIndividualEquipment: entitlement.OrganizationalClothingAndIndividualEquipment, - StorageInTransit: sit, - TotalDependents: totalDependents, - TotalWeight: totalWeight, - ETag: etag.GenerateEtag(entitlement.UpdatedAt), + StorageInTransit: sit, + TotalDependents: totalDependents, + TotalWeight: totalWeight, + WeightRestriction: &weightRestriction, + ETag: etag.GenerateEtag(entitlement.UpdatedAt), } } @@ -809,7 +814,7 @@ func MTOServiceItem(mtoServiceItem *models.MTOServiceItem) primemessages.MTOServ payload = &cratingSI case models.ReServiceCodeDDSHUT, models.ReServiceCodeDOSHUT: - payload = &primemessages.MTOServiceItemShuttle{ + payload = &primemessages.MTOServiceItemDomesticShuttle{ ReServiceCode: handlers.FmtString(string(mtoServiceItem.ReService.Code)), Reason: mtoServiceItem.Reason, EstimatedWeight: handlers.FmtPoundPtr(mtoServiceItem.EstimatedWeight), diff --git a/pkg/handlers/primeapi/payloads/model_to_payload_test.go b/pkg/handlers/primeapi/payloads/model_to_payload_test.go index 44cf2c2e40d..e54c61bd5fb 100644 --- a/pkg/handlers/primeapi/payloads/model_to_payload_test.go +++ b/pkg/handlers/primeapi/payloads/model_to_payload_test.go @@ -351,6 +351,7 @@ func (suite *PayloadsSuite) TestEntitlement() { ProGearWeightSpouse: 750, CreatedAt: time.Now(), UpdatedAt: time.Now(), + WeightRestriction: models.IntPointer(1000), } // TotalWeight needs to read from the internal weightAllotment, in this case 7000 lbs w/o dependents and @@ -373,6 +374,7 @@ func (suite *PayloadsSuite) TestEntitlement() { suite.Equal(true, payload.OrganizationalClothingAndIndividualEquipment) suite.Equal(int64(1000), payload.ProGearWeight) suite.Equal(int64(750), payload.ProGearWeightSpouse) + suite.Equal(int64(1000), *payload.WeightRestriction) suite.NotEmpty(payload.ETag) suite.Equal(etag.GenerateEtag(entitlement.UpdatedAt), payload.ETag) }) @@ -958,7 +960,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemDDSHUT() { suite.NotNil(resultDDSHUT) - _, ok := resultDDSHUT.(*primemessages.MTOServiceItemShuttle) + _, ok := resultDDSHUT.(*primemessages.MTOServiceItemDomesticShuttle) suite.True(ok) } diff --git a/pkg/handlers/primeapi/payloads/payload_to_model.go b/pkg/handlers/primeapi/payloads/payload_to_model.go index 53362a88b84..77d0db1e1f6 100644 --- a/pkg/handlers/primeapi/payloads/payload_to_model.go +++ b/pkg/handlers/primeapi/payloads/payload_to_model.go @@ -537,6 +537,14 @@ func MTOServiceItemModel(mtoServiceItem primemessages.MTOServiceItem) (*models.M model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primemessages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: + shuttleService := mtoServiceItem.(*primemessages.MTOServiceItemDomesticShuttle) + // values to get from payload + model.ReService.Code = models.ReServiceCode(*shuttleService.ReServiceCode) + model.Reason = shuttleService.Reason + model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) + model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primemessages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: shuttleService := mtoServiceItem.(*primemessages.MTOServiceItemInternationalShuttle) // values to get from payload diff --git a/pkg/handlers/primeapi/payloads/payload_to_model_test.go b/pkg/handlers/primeapi/payloads/payload_to_model_test.go index 9b5ec6f69a5..d45071aa7fa 100644 --- a/pkg/handlers/primeapi/payloads/payload_to_model_test.go +++ b/pkg/handlers/primeapi/payloads/payload_to_model_test.go @@ -65,7 +65,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { DCRTServiceItem.SetMoveTaskOrderID(handlers.FmtUUID(moveTaskOrderIDField)) DCRTServiceItem.SetMtoShipmentID(*mtoShipmentIDString) - DDSHUTServiceItem := &primemessages.MTOServiceItemShuttle{ + DDSHUTServiceItem := &primemessages.MTOServiceItemDomesticShuttle{ ReServiceCode: &ddshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, @@ -74,7 +74,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { DDSHUTServiceItem.SetMoveTaskOrderID(handlers.FmtUUID(moveTaskOrderIDField)) DDSHUTServiceItem.SetMtoShipmentID(*mtoShipmentIDString) - DOSHUTServiceItem := &primemessages.MTOServiceItemShuttle{ + DOSHUTServiceItem := &primemessages.MTOServiceItemDomesticShuttle{ ReServiceCode: &doshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, diff --git a/pkg/handlers/primeapiv2/move_task_order_test.go b/pkg/handlers/primeapiv2/move_task_order_test.go index 0b4fc4c56d8..f173af34bda 100644 --- a/pkg/handlers/primeapiv2/move_task_order_test.go +++ b/pkg/handlers/primeapiv2/move_task_order_test.go @@ -1190,7 +1190,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.NotNil(payload.ETag()) }) - suite.Run("Success - return all MTOServiceItemShuttle fields assoicated with the getMoveTaskOrder", func() { + suite.Run("Success - return all MTOServiceItemDomesticShuttle fields assoicated with the getMoveTaskOrder", func() { handler := GetMoveTaskOrderHandler{ suite.HandlerConfig(), movetaskorder.NewMoveTaskOrderFetcher(waf), @@ -1257,14 +1257,14 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { json, err := json.Marshal(serviceItemPayload) suite.NoError(err) - payload := primev2messages.MTOServiceItemShuttle{} + payload := primev2messages.MTOServiceItemDomesticShuttle{} err = payload.UnmarshalJSON(json) suite.NoError(err) suite.Equal(serviceItem.MoveTaskOrderID.String(), payload.MoveTaskOrderID().String()) suite.Equal(serviceItem.MTOShipmentID.String(), payload.MtoShipmentID().String()) suite.Equal(serviceItem.ID.String(), payload.ID().String()) - suite.Equal("MTOServiceItemShuttle", string(payload.ModelType())) + suite.Equal("MTOServiceItemDomesticShuttle", string(payload.ModelType())) suite.Equal(string(serviceItem.ReService.Code), string(*payload.ReServiceCode)) suite.Equal(serviceItem.ReService.Name, payload.ReServiceName()) suite.Equal(string(serviceItem.Status), string(payload.Status())) diff --git a/pkg/handlers/primeapiv2/mto_service_item.go b/pkg/handlers/primeapiv2/mto_service_item.go index 5188ccea511..495a597b88f 100644 --- a/pkg/handlers/primeapiv2/mto_service_item.go +++ b/pkg/handlers/primeapiv2/mto_service_item.go @@ -26,6 +26,7 @@ var CreateableServiceItemMap = map[primev2messages.MTOServiceItemModelType]bool{ primev2messages.MTOServiceItemModelTypeMTOServiceItemOriginSIT: true, primev2messages.MTOServiceItemModelTypeMTOServiceItemDestSIT: true, primev2messages.MTOServiceItemModelTypeMTOServiceItemShuttle: true, + primev2messages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: true, primev2messages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: true, primev2messages.MTOServiceItemModelTypeMTOServiceItemDomesticCrating: true, primev2messages.MTOServiceItemModelTypeMTOServiceItemInternationalCrating: true, diff --git a/pkg/handlers/primeapiv2/payloads/model_to_payload.go b/pkg/handlers/primeapiv2/payloads/model_to_payload.go index 3a49ba8b5fb..09f107a9e04 100644 --- a/pkg/handlers/primeapiv2/payloads/model_to_payload.go +++ b/pkg/handlers/primeapiv2/payloads/model_to_payload.go @@ -191,6 +191,10 @@ func Entitlement(entitlement *models.Entitlement) *primev2messages.Entitlements if entitlement.UBAllowance != nil { ubAllowance = int64(*entitlement.UBAllowance) } + var weightRestriction int64 + if entitlement.WeightRestriction != nil { + weightRestriction = int64(*entitlement.WeightRestriction) + } return &primev2messages.Entitlements{ ID: strfmt.UUID(entitlement.ID.String()), AuthorizedWeight: authorizedWeight, @@ -203,10 +207,11 @@ func Entitlement(entitlement *models.Entitlement) *primev2messages.Entitlements ProGearWeightSpouse: int64(entitlement.ProGearWeightSpouse), RequiredMedicalEquipmentWeight: int64(entitlement.RequiredMedicalEquipmentWeight), OrganizationalClothingAndIndividualEquipment: entitlement.OrganizationalClothingAndIndividualEquipment, - StorageInTransit: sit, - TotalDependents: totalDependents, - TotalWeight: totalWeight, - ETag: etag.GenerateEtag(entitlement.UpdatedAt), + StorageInTransit: sit, + TotalDependents: totalDependents, + TotalWeight: totalWeight, + WeightRestriction: &weightRestriction, + ETag: etag.GenerateEtag(entitlement.UpdatedAt), } } @@ -720,7 +725,7 @@ func MTOServiceItem(mtoServiceItem *models.MTOServiceItem) primev2messages.MTOSe payload = &cratingSI case models.ReServiceCodeDDSHUT, models.ReServiceCodeDOSHUT: - payload = &primev2messages.MTOServiceItemShuttle{ + payload = &primev2messages.MTOServiceItemDomesticShuttle{ ReServiceCode: handlers.FmtString(string(mtoServiceItem.ReService.Code)), Reason: mtoServiceItem.Reason, EstimatedWeight: handlers.FmtPoundPtr(mtoServiceItem.EstimatedWeight), diff --git a/pkg/handlers/primeapiv2/payloads/model_to_payload_test.go b/pkg/handlers/primeapiv2/payloads/model_to_payload_test.go index cb69d490a44..2119ecd1a8e 100644 --- a/pkg/handlers/primeapiv2/payloads/model_to_payload_test.go +++ b/pkg/handlers/primeapiv2/payloads/model_to_payload_test.go @@ -303,6 +303,7 @@ func (suite *PayloadsSuite) TestEntitlement() { ProGearWeightSpouse: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), + WeightRestriction: models.IntPointer(1000), } payload := Entitlement(&entitlement) @@ -325,6 +326,7 @@ func (suite *PayloadsSuite) TestEntitlement() { suite.Equal(int64(0), payload.TotalDependents) suite.Equal(int64(0), payload.TotalWeight) suite.Equal(int64(0), *payload.UnaccompaniedBaggageAllowance) + suite.Equal(int64(1000), *payload.WeightRestriction) }) suite.Run("Success - Returns the entitlement payload with all optional fields populated", func() { @@ -868,7 +870,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemDDSHUT() { suite.NotNil(resultDDSHUT) - _, ok := resultDDSHUT.(*primev2messages.MTOServiceItemShuttle) + _, ok := resultDDSHUT.(*primev2messages.MTOServiceItemDomesticShuttle) suite.True(ok) } diff --git a/pkg/handlers/primeapiv2/payloads/payload_to_model.go b/pkg/handlers/primeapiv2/payloads/payload_to_model.go index b628bcc0502..b57e5ca541b 100644 --- a/pkg/handlers/primeapiv2/payloads/payload_to_model.go +++ b/pkg/handlers/primeapiv2/payloads/payload_to_model.go @@ -621,7 +621,6 @@ func MTOServiceItemModel(mtoServiceItem primev2messages.MTOServiceItem) (*models if model.SITDestinationFinalAddress != nil { model.SITDestinationFinalAddressID = &model.SITDestinationFinalAddress.ID } - case primev2messages.MTOServiceItemModelTypeMTOServiceItemShuttle: shuttleService := mtoServiceItem.(*primev2messages.MTOServiceItemShuttle) // values to get from payload @@ -630,6 +629,14 @@ func MTOServiceItemModel(mtoServiceItem primev2messages.MTOServiceItem) (*models model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primev2messages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: + shuttleService := mtoServiceItem.(*primev2messages.MTOServiceItemDomesticShuttle) + // values to get from payload + model.ReService.Code = models.ReServiceCode(*shuttleService.ReServiceCode) + model.Reason = shuttleService.Reason + model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) + model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primev2messages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: shuttleService := mtoServiceItem.(*primev2messages.MTOServiceItemInternationalShuttle) // values to get from payload diff --git a/pkg/handlers/primeapiv2/payloads/payload_to_model_test.go b/pkg/handlers/primeapiv2/payloads/payload_to_model_test.go index 3df180b58ea..5a1e7844ab6 100644 --- a/pkg/handlers/primeapiv2/payloads/payload_to_model_test.go +++ b/pkg/handlers/primeapiv2/payloads/payload_to_model_test.go @@ -52,7 +52,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { Length: &crateMeasurement, } - DDSHUTServiceItem := &primev2messages.MTOServiceItemShuttle{ + DDSHUTServiceItem := &primev2messages.MTOServiceItemDomesticShuttle{ ReServiceCode: &ddshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, @@ -61,7 +61,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { DDSHUTServiceItem.SetMoveTaskOrderID(handlers.FmtUUID(moveTaskOrderIDField)) DDSHUTServiceItem.SetMtoShipmentID(*mtoShipmentIDString) - DOSHUTServiceItem := &primev2messages.MTOServiceItemShuttle{ + DOSHUTServiceItem := &primev2messages.MTOServiceItemDomesticShuttle{ ReServiceCode: &doshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, diff --git a/pkg/handlers/primeapiv3/move_task_order.go b/pkg/handlers/primeapiv3/move_task_order.go index c725fe7210b..1c4978d43a0 100644 --- a/pkg/handlers/primeapiv3/move_task_order.go +++ b/pkg/handlers/primeapiv3/move_task_order.go @@ -106,14 +106,14 @@ func (h GetMoveTaskOrderHandler) Handle(params movetaskorderops.GetMoveTaskOrder /** End of Feature Flag **/ // Add oconus rate area information to payload - shipmentPostalCodeRateArea, err := h.shipmentRateAreaFinder.GetPrimeMoveShipmentOconusRateArea(appCtx, *mto) + shipmentPostalCodeRateArea, err := h.shipmentRateAreaFinder.GetPrimeMoveShipmentRateAreas(appCtx, *mto) if err != nil { appCtx.Logger().Error("primeapi.GetMoveTaskOrderHandler error", zap.Error(err)) return movetaskorderops.NewGetMoveTaskOrderInternalServerError().WithPayload( payloads.InternalServerError(handlers.FmtString(err.Error()), h.GetTraceIDFromRequest(params.HTTPRequest))), err } - moveTaskOrderPayload := payloads.MoveTaskOrderWithShipmentOconusRateArea(appCtx, mto, shipmentPostalCodeRateArea) + moveTaskOrderPayload := payloads.MoveTaskOrderWithShipmentRateAreas(appCtx, mto, shipmentPostalCodeRateArea) return movetaskorderops.NewGetMoveTaskOrderOK().WithPayload(moveTaskOrderPayload), nil }) diff --git a/pkg/handlers/primeapiv3/move_task_order_test.go b/pkg/handlers/primeapiv3/move_task_order_test.go index 852fb0358c8..06880f84230 100644 --- a/pkg/handlers/primeapiv3/move_task_order_test.go +++ b/pkg/handlers/primeapiv3/move_task_order_test.go @@ -46,7 +46,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { setupDefaultTestHandler := func() GetMoveTaskOrderHandler { mockShipmentRateAreaFinder := &mocks.ShipmentRateAreaFinder{} - mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentRateAreas", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("models.Move"), ).Return(nil, nil) @@ -1169,7 +1169,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { suite.NotNil(payload.ETag()) }) - suite.Run("Success - return all MTOServiceItemShuttle fields assoicated with the getMoveTaskOrder", func() { + suite.Run("Success - return all MTOServiceItemDomesticShuttle fields assoicated with the getMoveTaskOrder", func() { handler := setupDefaultTestHandler() successMove := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) @@ -1233,14 +1233,14 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { json, err := json.Marshal(serviceItemPayload) suite.NoError(err) - payload := primev3messages.MTOServiceItemShuttle{} + payload := primev3messages.MTOServiceItemDomesticShuttle{} err = payload.UnmarshalJSON(json) suite.NoError(err) suite.Equal(serviceItem.MoveTaskOrderID.String(), payload.MoveTaskOrderID().String()) suite.Equal(serviceItem.MTOShipmentID.String(), payload.MtoShipmentID().String()) suite.Equal(serviceItem.ID.String(), payload.ID().String()) - suite.Equal("MTOServiceItemShuttle", string(payload.ModelType())) + suite.Equal("MTOServiceItemDomesticShuttle", string(payload.ModelType())) suite.Equal(string(serviceItem.ReService.Code), string(*payload.ReServiceCode)) suite.Equal(serviceItem.ReService.Name, payload.ReServiceName()) suite.Equal(string(serviceItem.Status), string(payload.Status())) @@ -1459,7 +1459,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { }, } - mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentRateAreas", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("models.Move"), ).Return(&shipmentPostalCodeRateArea, nil) @@ -1538,7 +1538,7 @@ func (suite *HandlerSuite) TestGetMoveTaskOrder() { defaultAddress := factory.BuildAddress(suite.DB(), nil, nil) - mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentOconusRateArea", + mockShipmentRateAreaFinder.On("GetPrimeMoveShipmentRateAreas", mock.AnythingOfType("*appcontext.appContext"), mock.AnythingOfType("models.Move"), ).Return(nil, apperror.InternalServerError{}) diff --git a/pkg/handlers/primeapiv3/mto_service_item.go b/pkg/handlers/primeapiv3/mto_service_item.go index f3c16b46e60..d3ab85fac3b 100644 --- a/pkg/handlers/primeapiv3/mto_service_item.go +++ b/pkg/handlers/primeapiv3/mto_service_item.go @@ -26,6 +26,7 @@ var CreateableServiceItemMap = map[primev3messages.MTOServiceItemModelType]bool{ primev3messages.MTOServiceItemModelTypeMTOServiceItemOriginSIT: true, primev3messages.MTOServiceItemModelTypeMTOServiceItemDestSIT: true, primev3messages.MTOServiceItemModelTypeMTOServiceItemShuttle: true, + primev3messages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: true, primev3messages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: true, primev3messages.MTOServiceItemModelTypeMTOServiceItemDomesticCrating: true, primev3messages.MTOServiceItemModelTypeMTOServiceItemInternationalCrating: true, diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload.go b/pkg/handlers/primeapiv3/payloads/model_to_payload.go index d61f70f997c..aba7a9c1718 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload.go @@ -80,7 +80,7 @@ func MoveTaskOrder(appCtx appcontext.AppContext, moveTaskOrder *models.Move) *pr return payload } -func MoveTaskOrderWithShipmentOconusRateArea(appCtx appcontext.AppContext, moveTaskOrder *models.Move, shipmentRateArea *[]services.ShipmentPostalCodeRateArea) *primev3messages.MoveTaskOrder { +func MoveTaskOrderWithShipmentRateAreas(appCtx appcontext.AppContext, moveTaskOrder *models.Move, shipmentRateArea *[]services.ShipmentPostalCodeRateArea) *primev3messages.MoveTaskOrder { // create default payload var payload = MoveTaskOrder(appCtx, moveTaskOrder) @@ -219,6 +219,10 @@ func Entitlement(entitlement *models.Entitlement) *primev3messages.Entitlements if entitlement.UBAllowance != nil { ubAllowance = int64(*entitlement.UBAllowance) } + var weightRestriction int64 + if entitlement.WeightRestriction != nil { + weightRestriction = int64(*entitlement.WeightRestriction) + } return &primev3messages.Entitlements{ ID: strfmt.UUID(entitlement.ID.String()), AuthorizedWeight: authorizedWeight, @@ -230,10 +234,11 @@ func Entitlement(entitlement *models.Entitlement) *primev3messages.Entitlements ProGearWeightSpouse: int64(entitlement.ProGearWeightSpouse), RequiredMedicalEquipmentWeight: int64(entitlement.RequiredMedicalEquipmentWeight), OrganizationalClothingAndIndividualEquipment: entitlement.OrganizationalClothingAndIndividualEquipment, - StorageInTransit: sit, - TotalDependents: totalDependents, - TotalWeight: totalWeight, - ETag: etag.GenerateEtag(entitlement.UpdatedAt), + StorageInTransit: sit, + TotalDependents: totalDependents, + TotalWeight: totalWeight, + WeightRestriction: &weightRestriction, + ETag: etag.GenerateEtag(entitlement.UpdatedAt), } } @@ -354,14 +359,12 @@ func MTOAgents(mtoAgents *models.MTOAgents) *primev3messages.MTOAgents { if mtoAgents == nil { return nil } - agents := make(primev3messages.MTOAgents, len(*mtoAgents)) for i, m := range *mtoAgents { copyOfM := m // Make copy to avoid implicit memory aliasing of items from a range statement. agents[i] = MTOAgent(©OfM) } - return &agents } @@ -865,7 +868,7 @@ func MTOServiceItem(mtoServiceItem *models.MTOServiceItem) primev3messages.MTOSe payload = &cratingSI case models.ReServiceCodeDDSHUT, models.ReServiceCodeDOSHUT: - payload = &primev3messages.MTOServiceItemShuttle{ + payload = &primev3messages.MTOServiceItemDomesticShuttle{ ReServiceCode: handlers.FmtString(string(mtoServiceItem.ReService.Code)), Reason: mtoServiceItem.Reason, EstimatedWeight: handlers.FmtPoundPtr(mtoServiceItem.EstimatedWeight), diff --git a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go index 3ff9bf25b94..f0f7036eac6 100644 --- a/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go +++ b/pkg/handlers/primeapiv3/payloads/model_to_payload_test.go @@ -274,7 +274,7 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { }) // no ShipmentPostalCodeRateArea passed in - returnedModel := MoveTaskOrderWithShipmentOconusRateArea(suite.AppContextForTest(), newMove, nil) + returnedModel := MoveTaskOrderWithShipmentRateAreas(suite.AppContextForTest(), newMove, nil) suite.IsType(&primev3messages.MoveTaskOrder{}, returnedModel) suite.Equal(strfmt.UUID(newMove.ID.String()), returnedModel.ID) @@ -337,7 +337,7 @@ func (suite *PayloadsSuite) TestMoveTaskOrder() { }, } - returnedModel = MoveTaskOrderWithShipmentOconusRateArea(suite.AppContextForTest(), newMove, &shipmentPostalCodeRateArea) + returnedModel = MoveTaskOrderWithShipmentRateAreas(suite.AppContextForTest(), newMove, &shipmentPostalCodeRateArea) var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) for _, i := range shipmentPostalCodeRateArea { @@ -545,6 +545,7 @@ func (suite *PayloadsSuite) TestEntitlement() { ProGearWeightSpouse: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), + WeightRestriction: models.IntPointer(1000), } payload := Entitlement(&entitlement) @@ -567,6 +568,7 @@ func (suite *PayloadsSuite) TestEntitlement() { suite.Equal(int64(0), payload.TotalDependents) suite.Equal(int64(0), payload.TotalWeight) suite.Equal(int64(0), *payload.UnaccompaniedBaggageAllowance) + suite.Equal(int64(1000), *payload.WeightRestriction) }) suite.Run("Success - Returns the entitlement payload with all optional fields populated", func() { @@ -1145,7 +1147,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemDDSHUT() { suite.NotNil(resultDDSHUT) - _, ok := resultDDSHUT.(*primev3messages.MTOServiceItemShuttle) + _, ok := resultDDSHUT.(*primev3messages.MTOServiceItemDomesticShuttle) suite.True(ok) } diff --git a/pkg/handlers/primeapiv3/payloads/payload_to_model.go b/pkg/handlers/primeapiv3/payloads/payload_to_model.go index f33d8b2ff34..2acc20eb04a 100644 --- a/pkg/handlers/primeapiv3/payloads/payload_to_model.go +++ b/pkg/handlers/primeapiv3/payloads/payload_to_model.go @@ -786,7 +786,6 @@ func MTOServiceItemModel(mtoServiceItem primev3messages.MTOServiceItem) (*models if model.SITDestinationFinalAddress != nil { model.SITDestinationFinalAddressID = &model.SITDestinationFinalAddress.ID } - case primev3messages.MTOServiceItemModelTypeMTOServiceItemShuttle: shuttleService := mtoServiceItem.(*primev3messages.MTOServiceItemShuttle) // values to get from payload @@ -795,6 +794,14 @@ func MTOServiceItemModel(mtoServiceItem primev3messages.MTOServiceItem) (*models model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primev3messages.MTOServiceItemModelTypeMTOServiceItemDomesticShuttle: + shuttleService := mtoServiceItem.(*primev3messages.MTOServiceItemDomesticShuttle) + // values to get from payload + model.ReService.Code = models.ReServiceCode(*shuttleService.ReServiceCode) + model.Reason = shuttleService.Reason + model.EstimatedWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.EstimatedWeight) + model.ActualWeight = handlers.PoundPtrFromInt64Ptr(shuttleService.ActualWeight) + case primev3messages.MTOServiceItemModelTypeMTOServiceItemInternationalShuttle: shuttleService := mtoServiceItem.(*primev3messages.MTOServiceItemInternationalShuttle) // values to get from payload diff --git a/pkg/handlers/primeapiv3/payloads/payload_to_model_test.go b/pkg/handlers/primeapiv3/payloads/payload_to_model_test.go index fd9430379f0..4f12b050b83 100644 --- a/pkg/handlers/primeapiv3/payloads/payload_to_model_test.go +++ b/pkg/handlers/primeapiv3/payloads/payload_to_model_test.go @@ -64,7 +64,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { DCRTServiceItem.SetMoveTaskOrderID(handlers.FmtUUID(moveTaskOrderIDField)) DCRTServiceItem.SetMtoShipmentID(*mtoShipmentIDString) - DDSHUTServiceItem := &primev3messages.MTOServiceItemShuttle{ + DDSHUTServiceItem := &primev3messages.MTOServiceItemDomesticShuttle{ ReServiceCode: &ddshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, @@ -73,7 +73,7 @@ func (suite *PayloadsSuite) TestMTOServiceItemModel() { DDSHUTServiceItem.SetMoveTaskOrderID(handlers.FmtUUID(moveTaskOrderIDField)) DDSHUTServiceItem.SetMtoShipmentID(*mtoShipmentIDString) - DOSHUTServiceItem := &primev3messages.MTOServiceItemShuttle{ + DOSHUTServiceItem := &primev3messages.MTOServiceItemDomesticShuttle{ ReServiceCode: &doshutCode, Reason: &reason, EstimatedWeight: &estimatedWeight, diff --git a/pkg/handlers/supportapi/internal/payloads/model_to_payload.go b/pkg/handlers/supportapi/internal/payloads/model_to_payload.go index 12ebfd4b8a1..506c2251256 100644 --- a/pkg/handlers/supportapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/supportapi/internal/payloads/model_to_payload.go @@ -148,6 +148,7 @@ func Entitlement(entitlement *models.Entitlement) *supportmessages.Entitlement { if entitlement.UBAllowance != nil { ubAllowance = int64(*entitlement.UBAllowance) } + return &supportmessages.Entitlement{ ID: strfmt.UUID(entitlement.ID.String()), AuthorizedWeight: authorizedWeight, @@ -350,7 +351,7 @@ func MTOServiceItem(mtoServiceItem *models.MTOServiceItem) supportmessages.MTOSe StandaloneCrate: mtoServiceItem.StandaloneCrate, } case models.ReServiceCodeDDSHUT, models.ReServiceCodeDOSHUT: - payload = &supportmessages.MTOServiceItemShuttle{ + payload = &supportmessages.MTOServiceItemDomesticShuttle{ ReServiceCode: handlers.FmtString(string(mtoServiceItem.ReService.Code)), Reason: mtoServiceItem.Reason, EstimatedWeight: handlers.FmtPoundPtr(mtoServiceItem.EstimatedWeight), diff --git a/pkg/handlers/supportapi/move_task_order.go b/pkg/handlers/supportapi/move_task_order.go index 98862bfbcc3..41bc92fbf9c 100644 --- a/pkg/handlers/supportapi/move_task_order.go +++ b/pkg/handlers/supportapi/move_task_order.go @@ -63,9 +63,7 @@ func (h MakeMoveTaskOrderAvailableHandlerFunc) Handle(params movetaskorderops.Ma moveTaskOrderID := uuid.FromStringOrNil(params.MoveTaskOrderID) - mto, err := h.moveTaskOrderAvailabilityUpdater.MakeAvailableToPrime(appCtx, moveTaskOrderID, eTag, false, false) - - if err != nil { + handleError := func(err error) (middleware.Responder, error) { appCtx.Logger().Error("supportapi.MakeMoveTaskOrderAvailableHandlerFunc error", zap.Error(err)) switch typedErr := err.(type) { case apperror.NotFoundError: @@ -83,6 +81,16 @@ func (h MakeMoveTaskOrderAvailableHandlerFunc) Handle(params movetaskorderops.Ma } } + _, err := h.moveTaskOrderAvailabilityUpdater.ApproveMoveAndCreateServiceItems(appCtx, moveTaskOrderID, eTag, false, false) + if err != nil { + return handleError(err) + } + + mto, _, err := h.moveTaskOrderAvailabilityUpdater.MakeAvailableToPrime(appCtx, moveTaskOrderID) + if err != nil { + return handleError(err) + } + moveTaskOrderPayload := payloads.MoveTaskOrder(mto) return movetaskorderops.NewMakeMoveTaskOrderAvailableOK().WithPayload(moveTaskOrderPayload), nil diff --git a/pkg/models/errors.go b/pkg/models/errors.go index 2a1cdeeff0e..78011442f48 100644 --- a/pkg/models/errors.go +++ b/pkg/models/errors.go @@ -63,3 +63,6 @@ var ErrMissingDestinationAddress = errors.New("DESTINATION_ADDRESS_MISSING") // ErrUnsupportedShipmentType is used if the shipment type is not supported by a method var ErrUnsupportedShipmentType = errors.New("UNSUPPORTED_SHIPMENT_TYPE") + +// ErrInvalidFilterFormat is used if the param filter is not in the expected format +var ErrInvalidFilterFormat = errors.New("invalid filter format") diff --git a/pkg/models/move.go b/pkg/models/move.go index c84068b47ef..b740c85c5d0 100644 --- a/pkg/models/move.go +++ b/pkg/models/move.go @@ -217,18 +217,28 @@ func (m Move) GetDestinationGBLOC(db *pop.Connection) (string, error) { var newGBLOC string if *destinationAddress.IsOconus { - err := db.Load(&m.Orders, "ServiceMember") - if err != nil { - if err.Error() == RecordNotFoundErrorString { - return "", errors.WithMessage(err, "No Service Member found in the DB associated with moveID "+m.ID.String()) + if m.OrdersID != uuid.Nil { + err := db.Q().EagerPreload("ServiceMember"). + Find(&m.Orders, m.OrdersID) + if err != nil { + if errors.Cause(err).Error() == RecordNotFoundErrorString { + return "", ErrFetchNotFound + } + return "", err } - return "", err + } else { + return "", errors.WithMessage(ErrInvalidOrderID, "Orders ID must have a value in order to get the destination GBLOC") } - newGBLOCOconus, err := FetchAddressGbloc(db, *destinationAddress, m.Orders.ServiceMember) - if err != nil { - return "", err + + if m.Orders.ServiceMember.Affiliation != nil { + newGBLOCOconus, err := FetchAddressGbloc(db, *destinationAddress, m.Orders.ServiceMember) + if err != nil { + return "", err + } + newGBLOC = *newGBLOCOconus + } else { + return "", errors.Errorf("ServiceMember.Affiliation cannot be NULL for GetDestinationGBLOC") } - newGBLOC = *newGBLOCOconus } else { newGBLOCConus, err := FetchGBLOCForPostalCode(db, destinationAddress.PostalCode) if err != nil { diff --git a/pkg/models/payment_request.go b/pkg/models/payment_request.go index 120f7a1d4fb..8a57e05e0ae 100644 --- a/pkg/models/payment_request.go +++ b/pkg/models/payment_request.go @@ -89,6 +89,11 @@ type PaymentRequest struct { TPPSPaidInvoiceReports TPPSPaidInvoiceReportEntrys `has_many:"tpps_paid_invoice_reports" fk_id:"payment_request_number"` } +type PaymentRequestWithEarliestRequestedDate struct { + ID uuid.UUID `json:"id" db:"id"` + EarliestRequestedDate time.Time `db:"requested_at"` +} + // TableName overrides the table name used by Pop. func (p PaymentRequest) TableName() string { return "payment_requests" diff --git a/pkg/models/port_location.go b/pkg/models/port_location.go index 4d514a2a545..414006d9c82 100644 --- a/pkg/models/port_location.go +++ b/pkg/models/port_location.go @@ -3,7 +3,10 @@ package models import ( "time" + "github.com/gobuffalo/pop/v6" "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/apperror" ) type PortLocation struct { @@ -24,3 +27,12 @@ type PortLocation struct { func (l PortLocation) TableName() string { return "port_locations" } + +func FetchPortLocationByCode(db *pop.Connection, portCode string) (*PortLocation, error) { + portLocation := PortLocation{} + err := db.Eager("Port", "UsPostRegionCity").Where("is_active = TRUE").InnerJoin("ports p", "port_id = p.id").Where("p.port_code = $1", portCode).First(&portLocation) + if err != nil { + return nil, apperror.NewQueryError("PortLocation", err, "") + } + return &portLocation, err +} diff --git a/pkg/models/port_location_test.go b/pkg/models/port_location_test.go index c63a4e34e29..ae50c68880f 100644 --- a/pkg/models/port_location_test.go +++ b/pkg/models/port_location_test.go @@ -2,6 +2,7 @@ package models_test import ( "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" ) func (suite *ModelSuite) TestPortLocation() { @@ -24,3 +25,28 @@ func (suite *ModelSuite) TestPortLocation() { suite.Equal("port_locations", portLocation.TableName()) }) } + +func (suite *ModelSuite) TestFetchPortLocationByCode() { + suite.Run("Port location can be fetched when it exists", func() { + + portLocation := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "SEA", + }, + }, + }, nil) + suite.NotNil(portLocation) + + result, err := models.FetchPortLocationByCode(suite.AppContextForTest().DB(), "SEA") + suite.NotNil(result) + suite.NoError(err) + suite.Equal(portLocation.ID, result.ID) + }) + + suite.Run("Sends back an error when it does not exist", func() { + result, err := models.FetchPortLocationByCode(suite.AppContextForTest().DB(), "123") + suite.Nil(result) + suite.Error(err) + }) +} diff --git a/pkg/models/ppm_shipment.go b/pkg/models/ppm_shipment.go index 0737417207c..d53bdb5b3ac 100644 --- a/pkg/models/ppm_shipment.go +++ b/pkg/models/ppm_shipment.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" @@ -34,11 +35,14 @@ type PPMCloseout struct { RemainingIncentive *unit.Cents HaulPrice *unit.Cents HaulFSC *unit.Cents - HaulType HaulType + HaulType *HaulType DOP *unit.Cents DDP *unit.Cents PackPrice *unit.Cents UnpackPrice *unit.Cents + IntlPackPrice *unit.Cents + IntlUnpackPrice *unit.Cents + IntlLinehaulPrice *unit.Cents SITReimbursement *unit.Cents } @@ -319,3 +323,44 @@ func FetchPPMShipmentByPPMShipmentID(db *pop.Connection, ppmShipmentID uuid.UUID } return &ppmShipment, nil } + +type PPMIncentiveOCONUS struct { + TotalIncentive int `db:"total_incentive"` + PriceISLH int `db:"price_islh"` + PriceIHPK int `db:"price_ihpk"` + PriceIHUPK int `db:"price_ihupk"` + PriceFSC int `db:"price_fsc"` +} + +// a db function that will handle updating the estimated_incentive value +// this simulates pricing of a basic iHHG shipment with ISLH, IHPK, IHUPK, and the CONUS portion for a FSC +func CalculatePPMIncentive(db *pop.Connection, ppmID uuid.UUID, pickupAddressID uuid.UUID, destAddressID uuid.UUID, moveDate time.Time, mileage int, weight int, isEstimated bool, isActual bool, isMax bool) (*PPMIncentiveOCONUS, error) { + var incentive PPMIncentiveOCONUS + + err := db.RawQuery("SELECT * FROM calculate_ppm_incentive($1, $2, $3, $4, $5, $6, $7, $8, $9)", ppmID, pickupAddressID, destAddressID, moveDate, mileage, weight, isEstimated, isActual, isMax). + First(&incentive) + if err != nil { + return nil, fmt.Errorf("error calculating PPM incentive for PPM ID %s: %w", ppmID, err) + } + + return &incentive, nil +} + +type PPMSITCosts struct { + TotalSITCost int `db:"total_cost"` + PriceFirstDaySIT int `db:"price_first_day"` + PriceAddlDaySIT int `db:"price_addl_day"` +} + +// a db function that will handle calculating and returning the SIT costs related to a PPM shipment +func CalculatePPMSITCost(db *pop.Connection, ppmID uuid.UUID, addressID uuid.UUID, isOrigin bool, moveDate time.Time, weight int, sitDays int) (*PPMSITCosts, error) { + var costs PPMSITCosts + + err := db.RawQuery("SELECT * FROM calculate_ppm_SIT_cost($1, $2, $3, $4, $5, $6)", ppmID, addressID, isOrigin, moveDate, weight, sitDays). + First(&costs) + if err != nil { + return nil, fmt.Errorf("error calculating PPM SIT costs for PPM ID %s: %w", ppmID, err) + } + + return &costs, nil +} diff --git a/pkg/models/ppm_shipment_test.go b/pkg/models/ppm_shipment_test.go index 4def2567968..7dbeb27f545 100644 --- a/pkg/models/ppm_shipment_test.go +++ b/pkg/models/ppm_shipment_test.go @@ -7,6 +7,7 @@ import ( "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/testdatagen" "github.com/transcom/mymove/pkg/unit" @@ -125,3 +126,161 @@ func (suite *ModelSuite) TestPPMShipmentValidation() { }) } } + +func (suite *ModelSuite) TestCalculatePPMIncentive() { + suite.Run("success - receive PPM incentive when all values exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "74135") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + moveDate := time.Now() + mileage := 1000 + weight := 2000 + + incentives, err := models.CalculatePPMIncentive(suite.DB(), ppmShipment.ID, pickupAddress.ID, destAddress.ID, moveDate, mileage, weight, true, false, false) + suite.NoError(err) + suite.NotNil(incentives) + suite.NotNil(incentives.PriceFSC) + suite.NotNil(incentives.PriceIHPK) + suite.NotNil(incentives.PriceIHUPK) + suite.NotNil(incentives.PriceISLH) + suite.NotNil(incentives.TotalIncentive) + }) + + suite.Run("failure - contract doesn't exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + pickupUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "74135") + suite.FatalNoError(err) + pickupAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + IsOconus: models.BoolPointer(false), + UsPostRegionCityID: &pickupUSPRC.ID, + }, + }, + }, nil) + + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + destAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + moveDate := time.Now() + mileage := 1000 + weight := 2000 + + incentives, err := models.CalculatePPMIncentive(suite.DB(), ppmShipment.ID, pickupAddress.ID, destAddress.ID, moveDate, mileage, weight, true, false, false) + suite.Error(err) + suite.Nil(incentives) + }) +} + +func (suite *ModelSuite) TestCalculatePPMSITCost() { + suite.Run("success - receive PPM SIT costs when all values exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + moveDate := time.Now() + sitDays := 7 + weight := 2000 + + sitCost, err := models.CalculatePPMSITCost(suite.DB(), ppmShipment.ID, address.ID, false, moveDate, weight, sitDays) + suite.NoError(err) + suite.NotNil(sitCost) + suite.NotNil(sitCost.PriceAddlDaySIT) + suite.NotNil(sitCost.PriceFirstDaySIT) + suite.NotNil(sitCost.TotalSITCost) + }) + + suite.Run("failure - contract doesn't exist", func() { + ppmShipment := factory.BuildPPMShipment(suite.DB(), nil, nil) + destUSPRC, err := models.FindByZipCode(suite.AppContextForTest().DB(), "99505") + suite.FatalNoError(err) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + UsPostRegionCityID: &destUSPRC.ID, + }, + }, + }, nil) + + moveDate := time.Now() + sitDays := 7 + weight := 2000 + + sitCost, err := models.CalculatePPMSITCost(suite.DB(), ppmShipment.ID, address.ID, false, moveDate, weight, sitDays) + suite.Error(err) + suite.Nil(sitCost) + }) +} diff --git a/pkg/models/re_intl_other_price.go b/pkg/models/re_intl_other_price.go index b8dce673214..1d95b5fcdd8 100644 --- a/pkg/models/re_intl_other_price.go +++ b/pkg/models/re_intl_other_price.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" @@ -45,3 +46,36 @@ func (r *ReIntlOtherPrice) Validate(_ *pop.Connection) (*validate.Errors, error) &validators.IntIsGreaterThan{Field: r.PerUnitCents.Int(), Name: "PerUnitCents", Compared: -1}, ), nil } + +// fetches a row from re_intl_other_prices using passed in parameters +// gets the rate_area_id & is_peak_period based on values provided +func FetchReIntlOtherPrice(db *pop.Connection, addressID uuid.UUID, serviceID uuid.UUID, contractID uuid.UUID, referenceDate *time.Time) (*ReIntlOtherPrice, error) { + if addressID != uuid.Nil && serviceID != uuid.Nil && contractID != uuid.Nil && referenceDate != nil { + // need to get the rate area first + rateAreaID, err := FetchRateAreaID(db, addressID, &serviceID, contractID) + if err != nil { + return nil, fmt.Errorf("error fetching rate area id for shipment ID: %s, service ID %s, and contract ID: %s: %s", addressID, serviceID, contractID, err) + } + + var isPeakPeriod bool + err = db.RawQuery("SELECT is_peak_period($1)", referenceDate).First(&isPeakPeriod) + if err != nil { + return nil, fmt.Errorf("error checking if date is peak period with date: %s: %s", contractID, err) + } + + var reIntlOtherPrice ReIntlOtherPrice + err = db.Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", rateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return nil, fmt.Errorf("error fetching row from re_int_other_prices using rateAreaID %s, service ID %s, and contract ID: %s: %s", rateAreaID, serviceID, contractID, err) + } + + return &reIntlOtherPrice, nil + } + + return nil, fmt.Errorf("error value from re_intl_other_prices - required parameters not provided") +} diff --git a/pkg/models/re_intl_other_price_test.go b/pkg/models/re_intl_other_price_test.go index 69e9770c47a..8cde04c61d4 100644 --- a/pkg/models/re_intl_other_price_test.go +++ b/pkg/models/re_intl_other_price_test.go @@ -1,9 +1,13 @@ package models_test import ( + "time" + "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/testdatagen" ) func (suite *ModelSuite) TestReIntlOtherPriceValidation() { @@ -43,3 +47,76 @@ func (suite *ModelSuite) TestReIntlOtherPriceValidation() { suite.verifyValidationErrors(&intlOtherPrice, expErrors) }) } + +func (suite *ModelSuite) TestFetchReIntlOtherPrice() { + suite.Run("success - receive ReIntlOtherPrice when all values exist and are found", func() { + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + moveDate := time.Now() + + reIntlOtherPrice, err := models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, contract.ID, &moveDate) + suite.NoError(err) + suite.NotNil(reIntlOtherPrice) + suite.NotNil(reIntlOtherPrice.PerUnitCents) + }) + + suite.Run("failure - receive error when values aren't provided", func() { + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + + contract := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{}) + moveDate := time.Now() + + // no address + reIntlOtherPrice, err := models.FetchReIntlOtherPrice(suite.DB(), uuid.Nil, reService.ID, contract.ID, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no service ID + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, uuid.Nil, contract.ID, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no contract ID + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, uuid.Nil, &moveDate) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + + // no move date + reIntlOtherPrice, err = models.FetchReIntlOtherPrice(suite.DB(), address.ID, reService.ID, contract.ID, nil) + suite.Error(err) + suite.Nil(reIntlOtherPrice) + suite.Contains(err.Error(), "error value from re_intl_other_prices - required parameters not provided") + }) +} diff --git a/pkg/models/re_service.go b/pkg/models/re_service.go index bab371ac5d4..85136a4705a 100644 --- a/pkg/models/re_service.go +++ b/pkg/models/re_service.go @@ -1,12 +1,15 @@ package models import ( + "fmt" "time" "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" "github.com/gobuffalo/validate/v3/validators" "github.com/gofrs/uuid" + + "github.com/transcom/mymove/pkg/apperror" ) // ReServiceCode is the code of service @@ -224,3 +227,16 @@ func (r *ReService) Validate(_ *pop.Connection) (*validate.Errors, error) { &validators.StringIsPresent{Field: r.Name, Name: "Name"}, ), nil } + +func FetchReServiceByCode(db *pop.Connection, code ReServiceCode) (*ReService, error) { + var reServiceCode ReServiceCode + if code != reServiceCode { + reService := ReService{} + err := db.Where("code = ?", code).First(&reService) + if err != nil { + return nil, apperror.NewQueryError("ReService", err, "") + } + return &reService, err + } + return nil, fmt.Errorf("error fetching from re_services - required code not provided") +} diff --git a/pkg/models/re_service_test.go b/pkg/models/re_service_test.go index 929677faab7..41e60c55d73 100644 --- a/pkg/models/re_service_test.go +++ b/pkg/models/re_service_test.go @@ -23,3 +23,19 @@ func (suite *ModelSuite) TestReServiceValidation() { suite.verifyValidationErrors(&emptyReService, expErrors) }) } + +func (suite *ModelSuite) TestFetchReServiceBycode() { + suite.Run("success - receive ReService when code is provided", func() { + reService, err := models.FetchReServiceByCode(suite.DB(), models.ReServiceCodeIHPK) + suite.NoError(err) + suite.NotNil(reService) + }) + + suite.Run("failure - receive error when code is not provided", func() { + var blankReServiceCode models.ReServiceCode + reService, err := models.FetchReServiceByCode(suite.DB(), blankReServiceCode) + suite.Error(err) + suite.Nil(reService) + suite.Contains(err.Error(), "error fetching from re_services - required code not provided") + }) +} diff --git a/pkg/models/roles/roles.go b/pkg/models/roles/roles.go index 10fedc99b6e..dd326a3f895 100644 --- a/pkg/models/roles/roles.go +++ b/pkg/models/roles/roles.go @@ -94,3 +94,25 @@ func FetchRolesForUser(db *pop.Connection, userID uuid.UUID) (Roles, error) { All(&roles) return roles, err } + +// Fetch like roles based on the search parameter +func FindRoles(db *pop.Connection, search string) (Roles, error) { + var rolesList Roles + + // The % operator filters out strings that are below this similarity threshold + err := db.Q().RawQuery("SET pg_trgm.similarity_threshold = 0.03").Exec() + if err != nil { + return rolesList, err + } + + sqlQuery := `select * from roles where role_name % $1` + + query := db.Q().RawQuery(sqlQuery, search) + if err := query.All(&rolesList); err != nil { + if err != nil { + return rolesList, err + } + } + + return rolesList, nil +} diff --git a/pkg/models/roles/roles_test.go b/pkg/models/roles/roles_test.go index 9ec5a0e7d86..6bcf0c07704 100644 --- a/pkg/models/roles/roles_test.go +++ b/pkg/models/roles/roles_test.go @@ -3,10 +3,12 @@ package roles_test import ( "testing" + "github.com/gofrs/uuid" "github.com/stretchr/testify/suite" "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" m "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/testingsuite" ) @@ -66,3 +68,36 @@ func (suite *RolesSuite) TestFetchRolesForUser() { suite.NoError(err) suite.Equal(1, len(userRoles), userRoles) } + +func (suite *RolesSuite) TestFindRoles() { + id1, _ := uuid.NewV4() + role1 := roles.Role{ + ID: id1, + RoleName: "Task Invoicing Officer", + RoleType: "role1", + } + + id2, _ := uuid.NewV4() + role2 := roles.Role{ + ID: id2, + RoleName: "Task Ordering Officer", + RoleType: "role2", + } + + id3, _ := uuid.NewV4() + role3 := roles.Role{ + ID: id3, + RoleName: "Contracting Officer", + RoleType: "role3", + } + + // Create roles + rs := roles.Roles{role1, role2, role3} + err := suite.DB().Create(rs) + suite.NoError(err) + + userRoles, err := m.FindRoles(suite.DB(), "Ta") + + suite.NoError(err) + suite.GreaterOrEqual(len(userRoles), 2) +} diff --git a/pkg/notifications/move_counseled.go b/pkg/notifications/move_counseled.go index e519fdcc6a7..76b4728bb14 100644 --- a/pkg/notifications/move_counseled.go +++ b/pkg/notifications/move_counseled.go @@ -6,6 +6,7 @@ import ( html "html/template" text "text/template" + "github.com/dustin/go-humanize" "github.com/gofrs/uuid" "go.uber.org/zap" @@ -91,12 +92,21 @@ func (m MoveCounseled) emails(appCtx appcontext.AppContext) ([]emailContent, err return emails, fmt.Errorf("no email found for service member") } + var weightRestriction *int64 + var weightRestrictionFormatted string + if orders.Entitlement != nil && orders.Entitlement.WeightRestriction != nil { + weightRestrictionInt64 := int64(*orders.Entitlement.WeightRestriction) + weightRestriction = &weightRestrictionInt64 + weightRestrictionFormatted = humanize.Comma(int64(*weightRestriction)) + } + htmlBody, textBody, err := m.renderTemplates(appCtx, MoveCounseledEmailData{ OriginDutyLocation: originDutyLocationName, DestinationLocation: destinationAddress, Locator: move.Locator, MyMoveLink: MyMoveLink, ActualExpenseReimbursement: actualExpenseReimbursement, + WeightRestriction: weightRestrictionFormatted, }) if err != nil { @@ -115,8 +125,8 @@ func (m MoveCounseled) emails(appCtx appcontext.AppContext) ([]emailContent, err // TODO: Send email to trusted contacts when that's supported return append(emails, smEmail), nil -} +} func (m MoveCounseled) renderTemplates(appCtx appcontext.AppContext, data MoveCounseledEmailData) (string, string, error) { htmlBody, err := m.RenderHTML(appCtx, data) if err != nil { @@ -135,6 +145,7 @@ type MoveCounseledEmailData struct { Locator string MyMoveLink string ActualExpenseReimbursement bool + WeightRestriction string } // RenderHTML renders the html for the email diff --git a/pkg/notifications/move_counseled_test.go b/pkg/notifications/move_counseled_test.go index a66370c27af..b2a5c736d48 100644 --- a/pkg/notifications/move_counseled_test.go +++ b/pkg/notifications/move_counseled_test.go @@ -273,3 +273,56 @@ func (suite *NotificationSuite) TestMoveCounseledDestinationIsDutyStationForPpmP suite.Contains(email.htmlBody, "from "+move.Orders.OriginDutyLocation.Name+" to "+move.Orders.NewDutyLocation.Name+" in the ") suite.Contains(email.textBody, "from "+move.Orders.OriginDutyLocation.Name+" to "+move.Orders.NewDutyLocation.Name+" in the ") } + +func (suite *NotificationSuite) TestMoveCounseledHTMLTemplateRenderWithWeightRestriction() { + approver := factory.BuildUser(nil, nil, nil) + move := factory.BuildMove(suite.DB(), nil, nil) + notification := NewMoveCounseled(move.ID) + + originDutyLocation := "origDutyLocation" + + s := MoveCounseledEmailData{ + OriginDutyLocation: &originDutyLocation, + DestinationLocation: "destDutyLocation", + Locator: "abc123", + MyMoveLink: MyMoveLink, + ActualExpenseReimbursement: true, + WeightRestriction: "1500", + } + + expectedHTMLContent := `

*** DO NOT REPLY directly to this email ***

+

This is a confirmation that your counselor has approved move details for the assigned move code abc123 from origDutyLocation to destDutyLocation in the MilMove system.

Your move has been identified as going to an administratively restricted HHG weight location. Your weight restriction is 1500lbs.

+

Be advised, you may be required to pay excess cost if you choose to move more than your weight restriction.

+

What this means to you:
+If you are doing a Personally Procured Move (PPM), you can start moving your personal property.

+

Next steps for a PPM: +

+

Next steps for government arranged shipments:
+

+

Thank you,
+USTRANSCOM MilMove Team

+

The information contained in this email may contain Privacy Act information and is therefore protected under the Privacy Act of 1974. Failure to protect Privacy Act information could result in a $5,000 fine.

` + + htmlContent, err := notification.RenderHTML(suite.AppContextWithSessionForTest(&auth.Session{ + UserID: approver.ID, + ApplicationName: auth.OfficeApp, + }), s) + + suite.NoError(err) + suite.Equal(trimExtraSpaces(expectedHTMLContent), trimExtraSpaces(htmlContent)) +} diff --git a/pkg/notifications/move_submitted_test.go b/pkg/notifications/move_submitted_test.go index b636d26a536..2d1693e79c1 100644 --- a/pkg/notifications/move_submitted_test.go +++ b/pkg/notifications/move_submitted_test.go @@ -273,10 +273,11 @@ func (suite *NotificationSuite) TestMoveSubmittedHTMLTemplateRenderWithGovCounse

- Your weight allowance: 7,999 pounds. + Your standard weight allowance: 7,999 pounds. That is how much combined weight the government will pay for all movements between authorized locations under your - orders. -

+ orders.

+

Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less.

+

If you move more than 7,999 pounds or ship to/from an other than authorized location, you may owe the @@ -385,10 +386,11 @@ func (suite *NotificationSuite) TestMoveSubmittedHTMLTemplateRenderWithoutGovCou

- Your weight allowance: 7,999 pounds. + Your standard weight allowance: 7,999 pounds. That is how much combined weight the government will pay for all movements between authorized locations under your - orders. -

+ orders.

+

Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less.

+

If you move more than 7,999 pounds or ship to/from an other than authorized location, you may owe the @@ -483,10 +485,11 @@ func (suite *NotificationSuite) TestMoveSubmittedHTMLTemplateRenderNoDutyLocatio

- Your weight allowance: 7,999 pounds. + Your standard weight allowance: 7,999 pounds. That is how much combined weight the government will pay for all movements between authorized locations under your - orders. -

+ orders.

+

Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less.

+

If you move more than 7,999 pounds or ship to/from an other than authorized location, you may owe the @@ -578,7 +581,7 @@ We have assigned you a move code: abc123. You can use this code when talking to To change any information about your move, or to add or cancel shipments, you should contact 555-555-5555 or visit your local transportation office (` + OneSourceTransportationOfficeLink + `) . -Your weight allowance: 7,999 pounds. That is how much combined weight the government will pay for all movements between authorized locations under your orders. +Your standard weight allowance: 7,999 pounds. That is how much combined weight the government will pay for all movements between authorized locations under your orders. Be advised, if you are moving to an administratively restricted HHG weight location this amount could be less. If you move more than 7,999 pounds or ship to/from an other than authorized location, you may owe the government the difference in cost between what you are authorized and what you decide to move. diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go index 0317726d032..acc87eb2715 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup.go @@ -48,6 +48,8 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service "Distance", "PickupAddress", "DestinationAddress", + "PPMShipment.PickupAddress", + "PPMShipment.DestinationAddress", ).Find(&mtoShipment, mtoShipment.ID) if err != nil { return "", err @@ -61,20 +63,47 @@ func (r DistanceZipLookup) lookup(appCtx appcontext.AppContext, keyData *Service // if the shipment is international, we need to change the respective ZIP to use the port ZIP and not the address ZIP if isInternationalShipment { - portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) - if err != nil { - return "", err - } - if portZip != nil && portType != nil { - // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) - // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) - if *portType == models.ReServiceCodePOEFSC.String() { - destinationZip = *portZip - } else if *portType == models.ReServiceCodePODFSC.String() { - pickupZip = *portZip + if mtoShipment.ShipmentType != models.MTOShipmentTypePPM { + portZip, portType, err := models.GetPortLocationInfoForShipment(appCtx.DB(), *mtoShipmentID) + if err != nil { + return "", err + } + if portZip != nil && portType != nil { + // if the port type is POEFSC this means the shipment is CONUS -> OCONUS (pickup -> port) + // if the port type is PODFSC this means the shipment is OCONUS -> CONUS (port -> destination) + if *portType == models.ReServiceCodePOEFSC.String() { + destinationZip = *portZip + } else if *portType == models.ReServiceCodePODFSC.String() { + pickupZip = *portZip + } + } else { + return "", apperror.NewNotFoundError(*mtoShipmentID, "looking for port ZIP for shipment") } } else { - return "", apperror.NewNotFoundError(*mtoShipmentID, "looking for port ZIP for shipment") + // PPMs get reimbursed for their travel from CONUS <-> Port ZIPs, but only for the Tacoma Port + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") // Tacoma port code + if err != nil { + return "", fmt.Errorf("unable to find port zip with code %s", "4E1") + } + if mtoShipment.PPMShipment != nil && mtoShipment.PPMShipment.PickupAddress != nil && mtoShipment.PPMShipment.DestinationAddress != nil { + // need to figure out if we are going to go Port -> CONUS or CONUS -> Port + pickupOconus := *mtoShipment.PPMShipment.PickupAddress.IsOconus + destOconus := *mtoShipment.PPMShipment.DestinationAddress.IsOconus + if pickupOconus && !destOconus { + // Port ZIP -> CONUS ZIP + pickupZip = portLocation.UsPostRegionCity.UsprZipID + destinationZip = mtoShipment.PPMShipment.DestinationAddress.PostalCode + } else if !pickupOconus && destOconus { + // CONUS ZIP -> Port ZIP + pickupZip = mtoShipment.PPMShipment.PickupAddress.PostalCode + destinationZip = portLocation.UsPostRegionCity.UsprZipID + } else { + // OCONUS -> OCONUS mileage they don't get reimbursed for this + return strconv.Itoa(0), nil + } + } else { + return "", fmt.Errorf("missing required PPM & address information for shipment with id %s", mtoShipmentID) + } } } errorMsgForPickupZip := fmt.Sprintf("Shipment must have valid pickup zipcode. Received: %s", pickupZip) diff --git a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go index 48c920919d0..8b8d866b313 100644 --- a/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/distance_zip_lookup_test.go @@ -125,6 +125,56 @@ func (suite *ServiceParamValueLookupsSuite) TestDistanceLookup() { suite.Equal(unit.Miles(defaultInternationalZipDistance), *mtoShipment.Distance) }) + suite.Run("Call ZipTransitDistance on international PPMs with CONUS -> Tacoma Port ZIP", func() { + miles := unit.Miles(defaultZipDistance) + + ppmShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + Distance: &miles, + ShipmentType: models.MTOShipmentTypePPM, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + distanceZipLookup := DistanceZipLookup{ + PickupAddress: models.Address{PostalCode: ppmShipment.PickupAddress.PostalCode}, + DestinationAddress: models.Address{PostalCode: ppmShipment.DestinationAddress.PostalCode}, + } + + appContext := suite.AppContextForTest() + distance, err := distanceZipLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppmShipment.ShipmentID, + }) + suite.NoError(err) + suite.NotNil(distance) + + planner := suite.planner.(*mocks.Planner) + // should be called with the 98421 ZIP of the Tacoma port and NOT 99505 + planner.AssertCalled(suite.T(), "ZipTransitDistance", appContext, ppmShipment.PickupAddress.PostalCode, "98421", true) + }) + suite.Run("Calculate transit zip distance with an approved Destination SIT service item", func() { testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go index f8efda9d451..37032a5ccf9 100644 --- a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup.go @@ -2,6 +2,9 @@ package serviceparamvaluelookups import ( "fmt" + "time" + + "github.com/gofrs/uuid" "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" @@ -16,19 +19,67 @@ type PerUnitCentsLookup struct { func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemParamKeyData) (string, error) { serviceID := p.ServiceItem.ReServiceID + if serviceID == uuid.Nil { + reService, err := models.FetchReServiceByCode(appCtx.DB(), p.ServiceItem.ReService.Code) + if err != nil { + return "", fmt.Errorf("error fetching ReService Code %s: %w", p.ServiceItem.ReService.Code, err) + } + serviceID = reService.ID + } contractID := s.ContractID - if p.MTOShipment.RequestedPickupDate == nil { - return "", fmt.Errorf("requested pickup date is required for shipment with id: %s", p.MTOShipment.ID) + var shipmentID uuid.UUID + var pickupAddressID uuid.UUID + var destinationAddressID uuid.UUID + var moveDate time.Time + // HHG shipment + if p.MTOShipment.ShipmentType != models.MTOShipmentTypePPM { + shipmentID = p.MTOShipment.ID + if p.MTOShipment.RequestedPickupDate != nil { + moveDate = *p.MTOShipment.RequestedPickupDate + } else { + return "", fmt.Errorf("requested pickup date is required for shipment with id: %s", shipmentID) + } + if p.MTOShipment.PickupAddressID != nil { + pickupAddressID = *p.MTOShipment.PickupAddressID + } else { + return "", fmt.Errorf("pickup address is required for shipment with id: %s", shipmentID) + } + if p.MTOShipment.DestinationAddressID != nil { + destinationAddressID = *p.MTOShipment.DestinationAddressID + } else { + return "", fmt.Errorf("destination address is required for shipment with id: %s", shipmentID) + } + } else { // PPM shipment + shipmentID = p.MTOShipment.PPMShipment.ID + if p.MTOShipment.ActualPickupDate != nil { + moveDate = *p.MTOShipment.ActualPickupDate + } else if p.MTOShipment.RequestedPickupDate != nil { + moveDate = *p.MTOShipment.RequestedPickupDate + } else { + return "", fmt.Errorf("actual move date is required for PPM shipment with id: %s", shipmentID) + } + + if p.MTOShipment.PPMShipment.PickupAddressID != nil { + pickupAddressID = *p.MTOShipment.PPMShipment.PickupAddressID + } else { + return "", fmt.Errorf("pickup address is required for PPM shipment with id: %s", shipmentID) + } + + if p.MTOShipment.PPMShipment.DestinationAddressID != nil { + destinationAddressID = *p.MTOShipment.PPMShipment.DestinationAddressID + } else { + return "", fmt.Errorf("destination address is required for PPM shipment with id: %s", shipmentID) + } } switch p.ServiceItem.ReService.Code { case models.ReServiceCodeIHPK: // IHPK: Need rate area ID for the pickup address - rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, &serviceID, contractID) + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), pickupAddressID, &serviceID, contractID) if err != nil { - return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlOtherPrice models.ReIntlOtherPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -43,11 +94,11 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP case models.ReServiceCodeIHUPK: // IHUPK: Need rate area ID for the destination address - rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, &serviceID, contractID) + rateAreaID, err := models.FetchRateAreaID(appCtx.DB(), destinationAddressID, &serviceID, contractID) if err != nil { - return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + return "", fmt.Errorf("error fetching rate area id for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlOtherPrice models.ReIntlOtherPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -62,15 +113,15 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP case models.ReServiceCodeISLH: // ISLH: Need rate area IDs for origin and destination - originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.PickupAddressID, &serviceID, contractID) + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), pickupAddressID, &serviceID, contractID) if err != nil { - return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), *p.MTOShipment.DestinationAddressID, &serviceID, contractID) + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), destinationAddressID, &serviceID, contractID) if err != nil { - return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", p.MTOShipment.ID, serviceID, err) + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) } - isPeakPeriod := ghcrateengine.IsPeakPeriod(*p.MTOShipment.RequestedPickupDate) + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) var reIntlPrice models.ReIntlPrice err = appCtx.DB().Q(). Where("contract_id = ?", contractID). @@ -84,6 +135,82 @@ func (p PerUnitCentsLookup) lookup(appCtx appcontext.AppContext, s *ServiceItemP } return reIntlPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + case models.ReServiceCodeIOFSIT: + // IOFSIT: Need rate area ID for origin + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), pickupAddressID, &serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", originRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IOFSIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, originRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, originRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIOASIT: + // IOASIT: Need rate area ID for origin + originRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), pickupAddressID, &serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for origin address for shipment ID: %s, service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", originRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IOASIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, originRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, originRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIDFSIT: + // IDFSIT: Need rate area ID for destination + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), destinationAddressID, &serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s, service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", destRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IDFSIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, destRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, destRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + + case models.ReServiceCodeIDASIT: + // IDASIT: Need rate area ID for destination + destRateAreaID, err := models.FetchRateAreaID(appCtx.DB(), destinationAddressID, &serviceID, contractID) + if err != nil { + return "", fmt.Errorf("error fetching rate area id for destination address for shipment ID: %s and service ID %s: %s", shipmentID, serviceID, err) + } + isPeakPeriod := ghcrateengine.IsPeakPeriod(moveDate) + var reIntlOtherPrice models.ReIntlOtherPrice + err = appCtx.DB().Q(). + Where("contract_id = ?", contractID). + Where("service_id = ?", serviceID). + Where("is_peak_period = ?", isPeakPeriod). + Where("rate_area_id = ?", destRateAreaID). + First(&reIntlOtherPrice) + if err != nil { + return "", fmt.Errorf("error fetching IDASIT per unit cents for contractID: %s, serviceID %s, isPeakPeriod: %t, destRateAreaID: %s: %s", contractID, serviceID, isPeakPeriod, destRateAreaID, err) + } + return reIntlOtherPrice.PerUnitCents.ToMillicents().ToCents().String(), nil + default: return "", fmt.Errorf("unsupported service code to retrieve service item param PerUnitCents") } diff --git a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go index 9937f86217b..dc69b69f888 100644 --- a/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/per_unit_cents_lookup_test.go @@ -30,6 +30,105 @@ func (suite *ServiceParamValueLookupsSuite) TestPerUnitCentsLookup() { } + setupTestDataPickupOCONUS := func(serviceCode models.ReServiceCode) models.Move { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + PickupAddressID: &address.ID, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + }, nil) + + return move + } + + setupTestDataDestOCONUS := func(serviceCode models.ReServiceCode) models.Move { + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + address := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + DestinationAddressID: &address.ID, + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: shipment, + LinkOnly: true, + }, + { + Model: models.ReService{ + Code: serviceCode, + }, + }, + }, nil) + return move + } + suite.Run("success - returns perUnitCent value for IHPK", func() { setupTestData(models.ReServiceCodeIHPK) @@ -119,6 +218,115 @@ func (suite *ServiceParamValueLookupsSuite) TestPerUnitCentsLookup() { suite.Equal(perUnitCents, "1605") }) + suite.Run("success - returns perUnitCent value for IOFSIT", func() { + move := setupTestDataPickupOCONUS(models.ReServiceCodeIOFSIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "607") + }) + + suite.Run("success - returns perUnitCent value for IOASIT", func() { + move := setupTestDataPickupOCONUS(models.ReServiceCodeIOASIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "14") + }) + + suite.Run("success - returns perUnitCent value for IDFSIT", func() { + move := setupTestDataDestOCONUS(models.ReServiceCodeIDFSIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "607") + }) + + suite.Run("success - returns perUnitCent value for IDASIT", func() { + move := setupTestDataDestOCONUS(models.ReServiceCodeIDASIT) + + paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), move.ID, nil) + suite.FatalNoError(err) + + perUnitCents, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.FatalNoError(err) + suite.Equal(perUnitCents, "14") + }) + + suite.Run("success - returns perUnitCent value for IDASIT for a PPM", func() { + contractYear := testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualPickupDate: models.TimePointer(time.Now()), + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: models.ReService{ + Code: models.ReServiceCodeIDASIT, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + _, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + perUnitCentsLookup := PerUnitCentsLookup{ + ServiceItem: mtoServiceItem, + MTOShipment: ppm.Shipment, + } + + appContext := suite.AppContextForTest() + perUnitCents, err := perUnitCentsLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppm.ShipmentID, + ContractID: contractYear.ContractID, + }) + suite.NoError(err) + suite.Equal(perUnitCents, "14") + }) + suite.Run("failure - unauthorized service code", func() { setupTestData(models.ReServiceCodeDUPK) diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go index 3ea8be94315..9621c56cd8a 100644 --- a/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup.go @@ -15,14 +15,27 @@ type PortZipLookup struct { ServiceItem models.MTOServiceItem } -func (p PortZipLookup) lookup(appCtx appcontext.AppContext, _ *ServiceItemParamKeyData) (string, error) { +func (p PortZipLookup) lookup(appCtx appcontext.AppContext, keyData *ServiceItemParamKeyData) (string, error) { var portLocationID *uuid.UUID if p.ServiceItem.PODLocationID != nil { portLocationID = p.ServiceItem.PODLocationID } else if p.ServiceItem.POELocationID != nil { portLocationID = p.ServiceItem.POELocationID } else { - return "", fmt.Errorf("unable to find port zip for service item id: %s", p.ServiceItem.ID) + // for PPMs we need to send back the ZIP for the Tacoma Port, they are reimbursed for their CONUS <-> Port travel + shipment, err := models.FetchShipmentByID(appCtx.DB(), *keyData.mtoShipmentID) + if err != nil { + return "", fmt.Errorf("unable to find shipment with id %s", keyData.mtoShipmentID) + } + if shipment.ShipmentType == models.MTOShipmentTypePPM && shipment.MarketCode == models.MarketCodeInternational { + portLocation, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") + if err != nil { + return "", fmt.Errorf("unable to find port zip with code %s", "4E1") + } + return portLocation.UsPostRegionCity.UsprZipID, nil + } else { + return "", nil + } } var portLocation models.PortLocation err := appCtx.DB().Q(). diff --git a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go index 4410ba8e198..b06533a4e1f 100644 --- a/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/port_zip_lookup_test.go @@ -85,7 +85,76 @@ func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) }) - suite.Run("failure - no port zip on service item", func() { + suite.Run("success - returns PortZip value for Port Code 4E1 for PPMs", func() { + port := factory.FetchPortLocation(suite.DB(), []factory.Customization{ + { + Model: models.Port{ + PortCode: "4E1", + }, + }, + }, nil) + + contractYear := testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: time.Now().Add(-24 * time.Hour), + EndDate: time.Now().Add(24 * time.Hour), + }, + }) + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ActualPickupDate: models.TimePointer(time.Now()), + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + mtoServiceItem = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + }, nil) + + _, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) + suite.FatalNoError(err) + + portZipLookup := PortZipLookup{ + ServiceItem: mtoServiceItem, + } + + appContext := suite.AppContextForTest() + portZip, err := portZipLookup.lookup(appContext, &ServiceItemParamKeyData{ + planner: suite.planner, + mtoShipmentID: &ppm.ShipmentID, + ContractID: contractYear.ContractID, + }) + suite.NoError(err) + suite.Equal(portZip, port.UsPostRegionCity.UsprZipID) + }) + + suite.Run("returns nothing if shipment is HHG and service item does not have port info", func() { testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ StartDate: time.Now().Add(-24 * time.Hour), @@ -108,7 +177,8 @@ func (suite *ServiceParamValueLookupsSuite) TestPortZipLookup() { paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, uuid.Must(uuid.NewV4()), mtoServiceItem.MoveTaskOrderID, nil) suite.FatalNoError(err) - _, err = paramLookup.ServiceParamValue(suite.AppContextForTest(), key) - suite.Error(err) + portZip, err := paramLookup.ServiceParamValue(suite.AppContextForTest(), key) + suite.NoError(err) + suite.Equal(portZip, "") }) } diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go index 580ec02bc19..6815d250f28 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups.go @@ -27,7 +27,7 @@ type ServiceItemParamKeyData struct { paramCache *ServiceParamsCache } -func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.ServiceItemParamName]ServiceItemParamKeyLookup, mtoServiceItem models.MTOServiceItem, mtoShipment models.MTOShipment, contractCode string) ServiceItemParamKeyData { +func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.ServiceItemParamName]ServiceItemParamKeyLookup, mtoServiceItem models.MTOServiceItem, mtoShipment models.MTOShipment, contractCode string, contractID uuid.UUID) ServiceItemParamKeyData { return ServiceItemParamKeyData{ planner: planner, lookups: lookups, @@ -36,6 +36,7 @@ func NewServiceItemParamKeyData(planner route.Planner, lookups map[models.Servic mtoShipmentID: &mtoShipment.ID, MoveTaskOrderID: mtoShipment.MoveTaskOrderID, ContractCode: contractCode, + ContractID: contractID, } } @@ -213,8 +214,11 @@ func ServiceParamLookupInitialize( mtoShipment.DestinationAddress = &destinationAddress switch mtoServiceItem.ReService.Code { - case models.ReServiceCodeDDASIT, models.ReServiceCodeDDDSIT, models.ReServiceCodeDDFSIT, models.ReServiceCodeDDSFSC, models.ReServiceCodeDOASIT, models.ReServiceCodeDOPSIT, models.ReServiceCodeDOFSIT, models.ReServiceCodeDOSFSC: - err := appCtx.DB().Load(&mtoShipment, "SITDurationUpdates") + case models.ReServiceCodeDDASIT, models.ReServiceCodeDDDSIT, models.ReServiceCodeDDFSIT, + models.ReServiceCodeDDSFSC, models.ReServiceCodeDOASIT, models.ReServiceCodeDOPSIT, + models.ReServiceCodeDOFSIT, models.ReServiceCodeDOSFSC, models.ReServiceCodeIOFSIT, + models.ReServiceCodeIOASIT, models.ReServiceCodeIDFSIT, models.ReServiceCodeIDASIT: + err := appCtx.DB().Load(&mtoShipment, "SITDurationUpdates", "PPMShipment.PickupAddress", "PPMShipment.DestinationAddress") if err != nil { return nil, err } diff --git a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go index d9b197d44d5..34d4a025d2b 100644 --- a/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go +++ b/pkg/payment_request/service_param_value_lookups/service_param_value_lookups_test.go @@ -172,7 +172,7 @@ func (suite *ServiceParamValueLookupsSuite) setupTestMTOServiceItemWithEstimated // i don't think this function gets called for PPMs, but need to verify //paramLookup, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, mtoServiceItem, paymentRequest.ID, paymentRequest.MoveTaskOrderID, nil) //suite.FatalNoError(err) - paramLookup := NewServiceItemParamKeyData(suite.planner, serviceItemLookups, mtoServiceItem, mtoShipment, testdatagen.DefaultContractCode) + paramLookup := NewServiceItemParamKeyData(suite.planner, serviceItemLookups, mtoServiceItem, mtoShipment, testdatagen.DefaultContractCode, uuid.Nil) return mtoServiceItem, paymentRequest, ¶mLookup } diff --git a/pkg/payment_request/service_param_value_lookups/service_params_cache_test.go b/pkg/payment_request/service_param_value_lookups/service_params_cache_test.go index f55832cc189..999db3de94b 100644 --- a/pkg/payment_request/service_param_value_lookups/service_params_cache_test.go +++ b/pkg/payment_request/service_param_value_lookups/service_params_cache_test.go @@ -20,7 +20,7 @@ type paramsCacheSubtestData struct { mtoServiceItemMS models.MTOServiceItem mtoServiceItemCrate1 models.MTOServiceItem mtoServiceItemCrate2 models.MTOServiceItem - mtoServiceItemShuttle models.MTOServiceItem + mtoServiceItemDomesticShuttle models.MTOServiceItem paramKeyWeightEstimated models.ServiceItemParamKey paramKeyRequestedPickupDate models.ServiceItemParamKey paramKeyMTOAvailableToPrimeAt models.ServiceItemParamKey @@ -224,7 +224,7 @@ func (suite *ServiceParamValueLookupsSuite) makeSubtestData() (subtestData *para subtestData.shuttleEstimatedWeight = unit.Pound(400) subtestData.shuttleActualWeight = unit.Pound(450) - subtestData.mtoServiceItemShuttle = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ + subtestData.mtoServiceItemDomesticShuttle = factory.BuildMTOServiceItem(suite.DB(), []factory.Customization{ { Model: subtestData.move, LinkOnly: true, @@ -248,7 +248,7 @@ func (suite *ServiceParamValueLookupsSuite) makeSubtestData() (subtestData *para // DOSHUT estimated weight factory.BuildServiceParam(suite.DB(), []factory.Customization{ { - Model: subtestData.mtoServiceItemShuttle.ReService, + Model: subtestData.mtoServiceItemDomesticShuttle.ReService, LinkOnly: true, }, { @@ -462,7 +462,7 @@ func (suite *ServiceParamValueLookupsSuite) TestServiceParamCache() { expected := strconv.Itoa(subtestData.estimatedWeight.Int()) suite.Equal(expected, estimatedWeightStr) - paramLookupService2, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, subtestData.mtoServiceItemShuttle, subtestData.paymentRequest.ID, subtestData.paymentRequest.MoveTaskOrderID, ¶mCache) + paramLookupService2, err := ServiceParamLookupInitialize(suite.AppContextForTest(), suite.planner, subtestData.mtoServiceItemDomesticShuttle, subtestData.paymentRequest.ID, subtestData.paymentRequest.MoveTaskOrderID, ¶mCache) suite.NoError(err) var shuttleEstimatedWeightStr string diff --git a/pkg/payment_request/service_param_value_lookups/weight_billed_lookup.go b/pkg/payment_request/service_param_value_lookups/weight_billed_lookup.go index bca7555ea28..2a3e2f8bb62 100644 --- a/pkg/payment_request/service_param_value_lookups/weight_billed_lookup.go +++ b/pkg/payment_request/service_param_value_lookups/weight_billed_lookup.go @@ -200,8 +200,13 @@ func applyMinimum(code models.ReServiceCode, shipmentType models.MTOShipmentType switch shipmentType { case models.MTOShipmentTypeUnaccompaniedBaggage: switch code { - case models.ReServiceCodeIOSHUT, - models.ReServiceCodeIDSHUT: + case models.ReServiceCodeUBP, + models.ReServiceCodeIUBPK, + models.ReServiceCodeIUBUPK, + models.ReServiceCodeIOSHUT, + models.ReServiceCodeIDSHUT, + models.ReServiceCodePODFSC, + models.ReServiceCodePOEFSC: if weight < 300 { result = 300 } @@ -238,16 +243,12 @@ func applyMinimum(code models.ReServiceCode, shipmentType models.MTOShipmentType models.ReServiceCodeIDDSIT, models.ReServiceCodeIOSHUT, models.ReServiceCodeIDSHUT, - models.ReServiceCodeFSC: + models.ReServiceCodeFSC, + models.ReServiceCodePODFSC, + models.ReServiceCodePOEFSC: if weight < 500 { result = 500 } - case models.ReServiceCodeUBP, - models.ReServiceCodeIUBPK, - models.ReServiceCodeIUBUPK: - if weight < 300 { - result = 300 - } } } return fmt.Sprintf("%d", result) diff --git a/pkg/payment_request/service_param_value_lookups/weight_billed_lookup_test.go b/pkg/payment_request/service_param_value_lookups/weight_billed_lookup_test.go index 78072940ae1..74ffcd88754 100644 --- a/pkg/payment_request/service_param_value_lookups/weight_billed_lookup_test.go +++ b/pkg/payment_request/service_param_value_lookups/weight_billed_lookup_test.go @@ -95,15 +95,15 @@ func (suite *ServiceParamValueLookupsSuite) TestWeightBilledLookup() { {models.ReServiceCodeDDDSIT, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, // International {models.ReServiceCodeISLH, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeUBP, unit.Pound(250), "300", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeISLH, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeUBP, unit.Pound(250), "300", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeISLH, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeUBP, unit.Pound(250), "300", models.MTOShipmentTypeHHG}, {models.ReServiceCodeIHPK, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, {models.ReServiceCodeIHUPK, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeIUBPK, unit.Pound(250), "300", models.MTOShipmentTypeHHG}, - {models.ReServiceCodeIUBUPK, unit.Pound(250), "300", models.MTOShipmentTypeHHG}, + {models.ReServiceCodePOEFSC, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, + {models.ReServiceCodePODFSC, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, + {models.ReServiceCodeUBP, unit.Pound(250), "300", models.MTOShipmentTypeUnaccompaniedBaggage}, + {models.ReServiceCodeIUBPK, unit.Pound(250), "300", models.MTOShipmentTypeUnaccompaniedBaggage}, + {models.ReServiceCodeIUBUPK, unit.Pound(250), "300", models.MTOShipmentTypeUnaccompaniedBaggage}, + {models.ReServiceCodePOEFSC, unit.Pound(250), "300", models.MTOShipmentTypeUnaccompaniedBaggage}, + {models.ReServiceCodePODFSC, unit.Pound(250), "300", models.MTOShipmentTypeUnaccompaniedBaggage}, // International SIT {models.ReServiceCodeIOFSIT, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, {models.ReServiceCodeIDFSIT, unit.Pound(450), "500", models.MTOShipmentTypeHHG}, diff --git a/pkg/services/event/ghc_endpoint.go b/pkg/services/event/ghc_endpoint.go index d24e480a02e..b06fa47d5d7 100644 --- a/pkg/services/event/ghc_endpoint.go +++ b/pkg/services/event/ghc_endpoint.go @@ -43,6 +43,9 @@ const GhcDeleteShipmentEndpointKey = "Ghc.DeleteShipment" // GhcApproveShipmentEndpointKey is the key for the approveShipment endpoint in ghc const GhcApproveShipmentEndpointKey = "Ghc.ApproveShipment" +// GhcApproveShipmentEndpointKey is the key for the approveShipment endpoint in ghc +const GhcApproveShipmentsEndpointKey = "Ghc.ApproveShipments" + // GhcRequestShipmentDiversionEndpointKey is the key for the requestShipmentDiversion endpoint in ghc const GhcRequestShipmentDiversionEndpointKey = "Ghc.RequestShipmentDiversion" @@ -180,6 +183,10 @@ var ghcEndpoints = EndpointMapType{ APIName: GhcAPIName, OperationID: "approveShipment", }, + GhcApproveShipmentsEndpointKey: { + APIName: GhcAPIName, + OperationID: "approveShipments", + }, GhcRequestShipmentDiversionEndpointKey: { APIName: GhcAPIName, OperationID: "requestShipmentDiversion", diff --git a/pkg/services/event/notification_test.go b/pkg/services/event/notification_test.go index eea593eba9a..ec669b81daf 100644 --- a/pkg/services/event/notification_test.go +++ b/pkg/services/event/notification_test.go @@ -200,7 +200,7 @@ func (suite *EventServiceSuite) Test_MTOServiceItemPayload() { }, }, }, nil) - data := &primemessages.MTOServiceItemShuttle{} + data := &primemessages.MTOServiceItemDomesticShuttle{} payload, assemblePayloadErr := assembleMTOServiceItemPayload(suite.AppContextForTest(), mtoServiceItemDOSHUT.ID) diff --git a/pkg/services/ghc_rate_engine.go b/pkg/services/ghc_rate_engine.go index 2aa23954ce6..d1c7b923118 100644 --- a/pkg/services/ghc_rate_engine.go +++ b/pkg/services/ghc_rate_engine.go @@ -280,3 +280,35 @@ type IntlPortFuelSurchargePricer interface { Price(appCtx appcontext.AppContext, actualPickupDate time.Time, distance unit.Miles, weight unit.Pound, fscWeightBasedDistanceMultiplier float64, eiaFuelPrice unit.Millicents) (unit.Cents, PricingDisplayParams, error) ParamsPricer } + +// IntlOriginFirstDaySITPricer prices international origin first day SIT +// +//go:generate mockery --name IntlOriginFirstDaySITPricer +type IntlOriginFirstDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlOriginAdditionalDaySITPricer prices international origin additional days of SIT +// +//go:generate mockery --name IntlOriginAdditionalDaySITPricer +type IntlOriginAdditionalDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlDestinationFirstDaySITPricer prices international destination first day SIT +// +//go:generate mockery --name IntlDestinationFirstDaySITPricer +type IntlDestinationFirstDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} + +// IntlDestinationAdditionalDaySITPricer prices international destination additional days of SIT +// +//go:generate mockery --name IntlDestinationAdditionalDaySITPricer +type IntlDestinationAdditionalDaySITPricer interface { + Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, PricingDisplayParams, error) + ParamsPricer +} diff --git a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go new file mode 100644 index 00000000000..ed57a28b57c --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer.go @@ -0,0 +1,50 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlDestinationAdditionalDaySITPricer struct { +} + +func NewIntlDestinationAdditionalDaySITPricer() services.IntlDestinationAdditionalDaySITPricer { + return &intlDestinationAdditionalDaySITPricer{} +} + +func (p intlDestinationAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlAdditionalDaySIT(appCtx, models.ReServiceCodeIDASIT, contractCode, referenceDate, numberOfDaysInSIT, weight, perUnitCents) +} + +func (p intlDestinationAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, numberOfDaysInSIT, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go new file mode 100644 index 00000000000..76a40753891 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_additional_days_sit_pricer_test.go @@ -0,0 +1,127 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + idasitTestContractYearName = "Base Period Year 1" + idasitTestPerUnitCents = unit.Cents(15000) + idasitTestTotalCost = unit.Cents(1575000) + idasitTestIsPeakPeriod = true + idasitTestEscalationCompounded = 1.0000 + idasitTestWeight = unit.Pound(2100) + idasitTestPriceCents = unit.Cents(500) + idasitNumerDaysInSIT = 5 +) + +var idasitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlDestinationAdditionalDaySITPricer() { + pricer := NewIntlDestinationAdditionalDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlDestinationAdditionalDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(idasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idasitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idasitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idasitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlDestinationAdditionalDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[4].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // NumberDaysSIT + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameNumberDaysSIT)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlDestinationAdditionalDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIDASIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: idasitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNameNumberDaysSIT, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idasitNumerDaysInSIT)), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idasitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(idasitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go new file mode 100644 index 00000000000..eb1354ebbd0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlDestinationFirstDaySITPricer struct { +} + +func NewIntlDestinationFirstDaySITPricer() services.IntlDestinationFirstDaySITPricer { + return &intlDestinationFirstDaySITPricer{} +} + +func (p intlDestinationFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlFirstDaySIT(appCtx, models.ReServiceCodeIDFSIT, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlDestinationFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go new file mode 100644 index 00000000000..fbaebec2df0 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_destination_first_day_sit_pricer_test.go @@ -0,0 +1,116 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + idfsitTestContractYearName = "Base Period Year 1" + idfsitTestPerUnitCents = unit.Cents(15000) + idfsitTestTotalCost = unit.Cents(315000) + idfsitTestIsPeakPeriod = true + idfsitTestEscalationCompounded = 1.0000 + idfsitTestWeight = unit.Pound(2100) + idfsitTestPriceCents = unit.Cents(500) + idfsitNumerDaysInSIT = 5 +) + +var idfsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlDestinationFirstDaySITPricer() { + pricer := NewIntlDestinationFirstDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlDestinationFirstDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(idfsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idfsitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idfsitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idfsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idfsitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlDestinationFirstDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlDestinationFirstDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIDFSIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: idfsitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(idfsitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(idfsitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go new file mode 100644 index 00000000000..e9b4dc22478 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer.go @@ -0,0 +1,50 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlOriginAdditionalDaySITPricer struct { +} + +func NewIntlOriginAdditionalDaySITPricer() services.IntlOriginAdditionalDaySITPricer { + return &intlOriginAdditionalDaySITPricer{} +} + +func (p intlOriginAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlAdditionalDaySIT(appCtx, models.ReServiceCodeIOASIT, contractCode, referenceDate, numberOfDaysInSIT, weight, perUnitCents) +} + +func (p intlOriginAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + numberOfDaysInSIT, err := getParamInt(params, models.ServiceItemParamNameNumberDaysSIT) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, numberOfDaysInSIT, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go new file mode 100644 index 00000000000..87686d5110a --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_additional_days_sit_pricer_test.go @@ -0,0 +1,127 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + ioasitTestContractYearName = "Base Period Year 1" + ioasitTestPerUnitCents = unit.Cents(15000) + ioasitTestTotalCost = unit.Cents(1575000) + ioasitTestIsPeakPeriod = true + ioasitTestEscalationCompounded = 1.0000 + ioasitTestWeight = unit.Pound(2100) + ioasitTestPriceCents = unit.Cents(500) + ioasitNumerDaysInSIT = 5 +) + +var ioasitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlOriginAdditionalDaySITPricer() { + pricer := NewIntlOriginAdditionalDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlOriginAdditionalDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(ioasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ioasitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ioasitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ioasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ioasitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlOriginAdditionalDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[4].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // NumberDaysSIT + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameNumberDaysSIT)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlOriginAdditionalDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIOASIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: ioasitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNameNumberDaysSIT, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ioasitNumerDaysInSIT)), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(ioasitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(ioasitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go new file mode 100644 index 00000000000..2070d13835b --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer.go @@ -0,0 +1,45 @@ +package ghcrateengine + +import ( + "time" + + "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/unit" +) + +type intlOriginFirstDaySITPricer struct { +} + +func NewIntlOriginFirstDaySITPricer() services.IntlOriginFirstDaySITPricer { + return &intlOriginFirstDaySITPricer{} +} + +func (p intlOriginFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + return priceIntlFirstDaySIT(appCtx, models.ReServiceCodeIOFSIT, contractCode, referenceDate, weight, perUnitCents) +} + +func (p intlOriginFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + contractCode, err := getParamString(params, models.ServiceItemParamNameContractCode) + if err != nil { + return unit.Cents(0), nil, err + } + + referenceDate, err := getParamTime(params, models.ServiceItemParamNameReferenceDate) + if err != nil { + return unit.Cents(0), nil, err + } + + perUnitCents, err := getParamInt(params, models.ServiceItemParamNamePerUnitCents) + if err != nil { + return unit.Cents(0), nil, err + } + + weightBilled, err := getParamInt(params, models.ServiceItemParamNameWeightBilled) + if err != nil { + return unit.Cents(0), nil, err + } + + return p.Price(appCtx, contractCode, referenceDate, unit.Pound(weightBilled), perUnitCents) +} diff --git a/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go new file mode 100644 index 00000000000..ae6d9069fc4 --- /dev/null +++ b/pkg/services/ghcrateengine/intl_origin_first_day_sit_pricer_test.go @@ -0,0 +1,116 @@ +package ghcrateengine + +import ( + "fmt" + "strconv" + "time" + + "github.com/transcom/mymove/pkg/factory" + "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/services" + "github.com/transcom/mymove/pkg/testdatagen" + "github.com/transcom/mymove/pkg/unit" +) + +const ( + iofsitTestContractYearName = "Base Period Year 1" + iofsitTestPerUnitCents = unit.Cents(15000) + iofsitTestTotalCost = unit.Cents(315000) + iofsitTestIsPeakPeriod = true + iofsitTestEscalationCompounded = 1.0000 + iofsitTestWeight = unit.Pound(2100) + iofsitTestPriceCents = unit.Cents(500) + iofsitNumerDaysInSIT = 5 +) + +var iofsitTestRequestedPickupDate = time.Date(testdatagen.TestYear, peakStart.month, peakStart.day, 0, 0, 0, 0, time.UTC) + +func (suite *GHCRateEngineServiceSuite) TestIntlOriginFirstDaySITPricer() { + pricer := NewIntlOriginFirstDaySITPricer() + + suite.Run("success using PaymentServiceItemParams", func() { + paymentServiceItem := suite.setupIntlOriginFirstDayServiceItem() + + totalCost, displayParams, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.NoError(err) + suite.Equal(iofsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: iofsitTestContractYearName}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(iofsitTestPerUnitCents)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(iofsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(iofsitTestEscalationCompounded)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("invalid parameters to PriceUsingParams", func() { + paymentServiceItem := suite.setupIntlOriginFirstDayServiceItem() + + // WeightBilled + paymentServiceItem.PaymentServiceItemParams[3].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err := pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNameWeightBilled)) + + // PerUnitCents + paymentServiceItem.PaymentServiceItemParams[2].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to an int", models.ServiceItemParamNamePerUnitCents)) + + // ReferenceDate + paymentServiceItem.PaymentServiceItemParams[1].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a time", models.ServiceItemParamNameReferenceDate)) + + // ContractCode + paymentServiceItem.PaymentServiceItemParams[0].ServiceItemParamKey.Type = models.ServiceItemParamTypeBoolean + _, _, err = pricer.PriceUsingParams(suite.AppContextForTest(), paymentServiceItem.PaymentServiceItemParams) + suite.Error(err) + suite.Contains(err.Error(), fmt.Sprintf("trying to convert %s to a string", models.ServiceItemParamNameContractCode)) + }) +} + +func (suite *GHCRateEngineServiceSuite) setupIntlOriginFirstDayServiceItem() models.PaymentServiceItem { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2018, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2018, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + return factory.BuildPaymentServiceItemWithParams( + suite.DB(), + models.ReServiceCodeIOFSIT, + []factory.CreatePaymentServiceItemParams{ + { + Key: models.ServiceItemParamNameContractCode, + KeyType: models.ServiceItemParamTypeString, + Value: contract.Code, + }, + { + Key: models.ServiceItemParamNameReferenceDate, + KeyType: models.ServiceItemParamTypeDate, + Value: iofsitTestRequestedPickupDate.Format(DateParamFormat), + }, + { + Key: models.ServiceItemParamNamePerUnitCents, + KeyType: models.ServiceItemParamTypeInteger, + Value: fmt.Sprintf("%d", int(iofsitTestPerUnitCents)), + }, + { + Key: models.ServiceItemParamNameWeightBilled, + KeyType: models.ServiceItemParamTypeInteger, + Value: strconv.Itoa(iofsitTestWeight.Int()), + }, + }, nil, nil, + ) +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl.go b/pkg/services/ghcrateengine/pricer_helpers_intl.go index 7d8d70508ea..9754f552b3d 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_intl.go +++ b/pkg/services/ghcrateengine/pricer_helpers_intl.go @@ -114,3 +114,111 @@ func priceIntlPackUnpack(appCtx appcontext.AppContext, packUnpackCode models.ReS return totalCost, displayParams, nil } + +func priceIntlFirstDaySIT(appCtx appcontext.AppContext, firstDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if firstDaySITCode != models.ReServiceCodeIOFSIT && firstDaySITCode != models.ReServiceCodeIDFSIT { + return 0, nil, fmt.Errorf("unsupported first day SIT code of %s", firstDaySITCode) + } + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("ReferenceDate is required") + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear(appCtx, contract.ID, referenceDate, false, basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalCost := unit.Cents(math.Round(escalatedPrice)) + + displayParams := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + } + + return totalCost, displayParams, nil +} + +func priceIntlAdditionalDaySIT(appCtx appcontext.AppContext, additionalDaySITCode models.ReServiceCode, contractCode string, referenceDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + if additionalDaySITCode != models.ReServiceCodeIOASIT && additionalDaySITCode != models.ReServiceCodeIDASIT { + return 0, nil, fmt.Errorf("unsupported additional day SIT code of %s", additionalDaySITCode) + } + if len(contractCode) == 0 { + return 0, nil, errors.New("ContractCode is required") + } + if referenceDate.IsZero() { + return 0, nil, errors.New("ReferenceDate is required") + } + if numberOfDaysInSIT == 0 { + return 0, nil, errors.New("NumberDaysSIT is required") + } + if perUnitCents == 0 { + return 0, nil, errors.New("PerUnitCents is required") + } + + isPeakPeriod := IsPeakPeriod(referenceDate) + + contract, err := fetchContractByContractCode(appCtx, contractCode) + if err != nil { + return 0, nil, fmt.Errorf("could not find contract with code: %s: %w", contractCode, err) + } + + basePrice := float64(perUnitCents) + escalatedPrice, contractYear, err := escalatePriceForContractYear(appCtx, contract.ID, referenceDate, false, basePrice) + if err != nil { + return 0, nil, fmt.Errorf("could not calculate escalated price: %w", err) + } + + escalatedPrice = escalatedPrice * weight.ToCWTFloat64() + totalForNumberOfDaysPrice := escalatedPrice * float64(numberOfDaysInSIT) + totalCost := unit.Cents(math.Round(totalForNumberOfDaysPrice)) + + displayParams := services.PricingDisplayParams{ + { + Key: models.ServiceItemParamNameContractYearName, + Value: contractYear.Name, + }, + { + Key: models.ServiceItemParamNamePriceRateOrFactor, + Value: FormatCents(unit.Cents(perUnitCents)), + }, + { + Key: models.ServiceItemParamNameIsPeak, + Value: FormatBool(isPeakPeriod), + }, + { + Key: models.ServiceItemParamNameEscalationCompounded, + Value: FormatEscalation(contractYear.EscalationCompounded), + }, + } + + return totalCost, displayParams, nil +} diff --git a/pkg/services/ghcrateengine/pricer_helpers_intl_test.go b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go index 56d5bcce1dc..14e3d6c8618 100644 --- a/pkg/services/ghcrateengine/pricer_helpers_intl_test.go +++ b/pkg/services/ghcrateengine/pricer_helpers_intl_test.go @@ -99,3 +99,109 @@ func (suite *GHCRateEngineServiceSuite) TestPriceIntlPackUnpack() { }) } + +func (suite *GHCRateEngineServiceSuite) TestPriceIntlFirstDaySIT() { + suite.Run("success with IDFSIT", func() { + suite.setupIntlDestinationFirstDayServiceItem() + totalCost, displayParams, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(idfsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idfsitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idfsitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idfsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idfsitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("success with IOFSIT", func() { + suite.setupIntlOriginFirstDayServiceItem() + totalCost, displayParams, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIOFSIT, testdatagen.DefaultContractCode, iofsitTestRequestedPickupDate, iofsitTestWeight, iofsitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(iofsitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: iofsitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(iofsitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(iofsitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(iofsitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("Invalid parameters to Price", func() { + suite.setupIntlDestinationFirstDayServiceItem() + _, _, err := priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeDLH, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "unsupported first day SIT code") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, "", idfsitTestRequestedPickupDate, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ContractCode is required") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, time.Time{}, idfsitTestWeight, idfsitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ReferenceDate is required") + + _, _, err = priceIntlFirstDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDFSIT, testdatagen.DefaultContractCode, idfsitTestRequestedPickupDate, idfsitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "PerUnitCents is required") + }) +} + +func (suite *GHCRateEngineServiceSuite) TestPriceIntlAdditionalDaySIT() { + suite.Run("success with IDASIT", func() { + suite.setupIntlDestinationAdditionalDayServiceItem() + totalCost, displayParams, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(idasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: idasitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(idasitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(idasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(idasitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("success with IOASIT", func() { + suite.setupIntlOriginAdditionalDayServiceItem() + totalCost, displayParams, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIOASIT, testdatagen.DefaultContractCode, ioasitTestRequestedPickupDate, idasitNumerDaysInSIT, ioasitTestWeight, ioasitTestPerUnitCents.Int()) + suite.NoError(err) + suite.Equal(ioasitTestTotalCost, totalCost) + + expectedParams := services.PricingDisplayParams{ + {Key: models.ServiceItemParamNameContractYearName, Value: ioasitTestContractYearName}, + {Key: models.ServiceItemParamNameEscalationCompounded, Value: FormatEscalation(ioasitTestEscalationCompounded)}, + {Key: models.ServiceItemParamNameIsPeak, Value: FormatBool(ioasitTestIsPeakPeriod)}, + {Key: models.ServiceItemParamNamePriceRateOrFactor, Value: FormatCents(ioasitTestPerUnitCents)}, + } + suite.validatePricerCreatedParams(expectedParams, displayParams) + }) + + suite.Run("Invalid parameters to Price", func() { + suite.setupIntlDestinationAdditionalDayServiceItem() + _, _, err := priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeDLH, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "unsupported additional day SIT code") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, "", idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ContractCode is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, time.Time{}, idasitNumerDaysInSIT, idasitTestWeight, idasitTestPerUnitCents.Int()) + suite.Error(err) + suite.Contains(err.Error(), "ReferenceDate is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, idasitNumerDaysInSIT, idasitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "PerUnitCents is required") + + _, _, err = priceIntlAdditionalDaySIT(suite.AppContextForTest(), models.ReServiceCodeIDASIT, testdatagen.DefaultContractCode, idasitTestRequestedPickupDate, 0, idasitTestWeight, 0) + suite.Error(err) + suite.Contains(err.Error(), "NumberDaysSIT is required") + }) +} diff --git a/pkg/services/ghcrateengine/service_item_pricer.go b/pkg/services/ghcrateengine/service_item_pricer.go index 130c137c7c6..777ca2283bf 100644 --- a/pkg/services/ghcrateengine/service_item_pricer.go +++ b/pkg/services/ghcrateengine/service_item_pricer.go @@ -107,6 +107,14 @@ func PricerForServiceItem(serviceCode models.ReServiceCode) (services.ParamsPric return NewPortFuelSurchargePricer(), nil case models.ReServiceCodePODFSC: return NewPortFuelSurchargePricer(), nil + case models.ReServiceCodeIOFSIT: + return NewIntlOriginFirstDaySITPricer(), nil + case models.ReServiceCodeIOASIT: + return NewIntlOriginAdditionalDaySITPricer(), nil + case models.ReServiceCodeIDFSIT: + return NewIntlDestinationFirstDaySITPricer(), nil + case models.ReServiceCodeIDASIT: + return NewIntlDestinationAdditionalDaySITPricer(), nil default: // TODO: We may want a different error type here after all pricers have been implemented return nil, apperror.NewNotImplementedError(fmt.Sprintf("pricer not found for code %s", serviceCode)) diff --git a/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go b/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go new file mode 100644 index 00000000000..7a11b759d7f --- /dev/null +++ b/pkg/services/mocks/IntlDestinationAdditionalDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlDestinationAdditionalDaySITPricer is an autogenerated mock type for the IntlDestinationAdditionalDaySITPricer type +type IntlDestinationAdditionalDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents +func (_m *IntlDestinationAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlDestinationAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlDestinationAdditionalDaySITPricer creates a new instance of IntlDestinationAdditionalDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlDestinationAdditionalDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlDestinationAdditionalDaySITPricer { + mock := &IntlDestinationAdditionalDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go b/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go new file mode 100644 index 00000000000..99df36da131 --- /dev/null +++ b/pkg/services/mocks/IntlDestinationFirstDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlDestinationFirstDaySITPricer is an autogenerated mock type for the IntlDestinationFirstDaySITPricer type +type IntlDestinationFirstDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlDestinationFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlDestinationFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlDestinationFirstDaySITPricer creates a new instance of IntlDestinationFirstDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlDestinationFirstDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlDestinationFirstDaySITPricer { + mock := &IntlDestinationFirstDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go b/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go new file mode 100644 index 00000000000..a931b2d3879 --- /dev/null +++ b/pkg/services/mocks/IntlOriginAdditionalDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlOriginAdditionalDaySITPricer is an autogenerated mock type for the IntlOriginAdditionalDaySITPricer type +type IntlOriginAdditionalDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents +func (_m *IntlOriginAdditionalDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, numberOfDaysInSIT int, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, int, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, numberOfDaysInSIT, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlOriginAdditionalDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlOriginAdditionalDaySITPricer creates a new instance of IntlOriginAdditionalDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlOriginAdditionalDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlOriginAdditionalDaySITPricer { + mock := &IntlOriginAdditionalDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/IntlOriginFirstDaySITPricer.go b/pkg/services/mocks/IntlOriginFirstDaySITPricer.go new file mode 100644 index 00000000000..d36cb1e92c9 --- /dev/null +++ b/pkg/services/mocks/IntlOriginFirstDaySITPricer.go @@ -0,0 +1,109 @@ +// Code generated by mockery. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + appcontext "github.com/transcom/mymove/pkg/appcontext" + + models "github.com/transcom/mymove/pkg/models" + + services "github.com/transcom/mymove/pkg/services" + + time "time" + + unit "github.com/transcom/mymove/pkg/unit" +) + +// IntlOriginFirstDaySITPricer is an autogenerated mock type for the IntlOriginFirstDaySITPricer type +type IntlOriginFirstDaySITPricer struct { + mock.Mock +} + +// Price provides a mock function with given fields: appCtx, contractCode, requestedPickupDate, weight, perUnitCents +func (_m *IntlOriginFirstDaySITPricer) Price(appCtx appcontext.AppContext, contractCode string, requestedPickupDate time.Time, weight unit.Pound, perUnitCents int) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + + if len(ret) == 0 { + panic("no return value specified for Price") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) unit.Cents); ok { + r0 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) services.PricingDisplayParams); ok { + r1 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, string, time.Time, unit.Pound, int) error); ok { + r2 = rf(appCtx, contractCode, requestedPickupDate, weight, perUnitCents) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// PriceUsingParams provides a mock function with given fields: appCtx, params +func (_m *IntlOriginFirstDaySITPricer) PriceUsingParams(appCtx appcontext.AppContext, params models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error) { + ret := _m.Called(appCtx, params) + + if len(ret) == 0 { + panic("no return value specified for PriceUsingParams") + } + + var r0 unit.Cents + var r1 services.PricingDisplayParams + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) (unit.Cents, services.PricingDisplayParams, error)); ok { + return rf(appCtx, params) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, models.PaymentServiceItemParams) unit.Cents); ok { + r0 = rf(appCtx, params) + } else { + r0 = ret.Get(0).(unit.Cents) + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, models.PaymentServiceItemParams) services.PricingDisplayParams); ok { + r1 = rf(appCtx, params) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(services.PricingDisplayParams) + } + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, models.PaymentServiceItemParams) error); ok { + r2 = rf(appCtx, params) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIntlOriginFirstDaySITPricer creates a new instance of IntlOriginFirstDaySITPricer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIntlOriginFirstDaySITPricer(t interface { + mock.TestingT + Cleanup(func()) +}) *IntlOriginFirstDaySITPricer { + mock := &IntlOriginFirstDaySITPricer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/services/mocks/MoveTaskOrderUpdater.go b/pkg/services/mocks/MoveTaskOrderUpdater.go index 8eeaa09fcf3..ec3a1176939 100644 --- a/pkg/services/mocks/MoveTaskOrderUpdater.go +++ b/pkg/services/mocks/MoveTaskOrderUpdater.go @@ -16,12 +16,12 @@ type MoveTaskOrderUpdater struct { mock.Mock } -// MakeAvailableToPrime provides a mock function with given fields: appCtx, moveTaskOrderID, eTag, includeServiceCodeMS, includeServiceCodeCS -func (_m *MoveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, includeServiceCodeMS bool, includeServiceCodeCS bool) (*models.Move, error) { +// ApproveMoveAndCreateServiceItems provides a mock function with given fields: appCtx, moveTaskOrderID, eTag, includeServiceCodeMS, includeServiceCodeCS +func (_m *MoveTaskOrderUpdater) ApproveMoveAndCreateServiceItems(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, includeServiceCodeMS bool, includeServiceCodeCS bool) (*models.Move, error) { ret := _m.Called(appCtx, moveTaskOrderID, eTag, includeServiceCodeMS, includeServiceCodeCS) if len(ret) == 0 { - panic("no return value specified for MakeAvailableToPrime") + panic("no return value specified for ApproveMoveAndCreateServiceItems") } var r0 *models.Move @@ -46,6 +46,43 @@ func (_m *MoveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContex return r0, r1 } +// MakeAvailableToPrime provides a mock function with given fields: appCtx, moveTaskOrderID +func (_m *MoveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID) (*models.Move, bool, error) { + ret := _m.Called(appCtx, moveTaskOrderID) + + if len(ret) == 0 { + panic("no return value specified for MakeAvailableToPrime") + } + + var r0 *models.Move + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID) (*models.Move, bool, error)); ok { + return rf(appCtx, moveTaskOrderID) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID) *models.Move); ok { + r0 = rf(appCtx, moveTaskOrderID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Move) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID) bool); ok { + r1 = rf(appCtx, moveTaskOrderID) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(appcontext.AppContext, uuid.UUID) error); ok { + r2 = rf(appCtx, moveTaskOrderID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + // ShowHide provides a mock function with given fields: appCtx, moveTaskOrderID, show func (_m *MoveTaskOrderUpdater) ShowHide(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, show *bool) (*models.Move, error) { ret := _m.Called(appCtx, moveTaskOrderID, show) diff --git a/pkg/services/mocks/RequestedOfficeUserListFetcher.go b/pkg/services/mocks/RequestedOfficeUserListFetcher.go index 98a226808eb..98211d4b57b 100644 --- a/pkg/services/mocks/RequestedOfficeUserListFetcher.go +++ b/pkg/services/mocks/RequestedOfficeUserListFetcher.go @@ -8,6 +8,8 @@ import ( models "github.com/transcom/mymove/pkg/models" + pop "github.com/gobuffalo/pop/v6" + services "github.com/transcom/mymove/pkg/services" ) @@ -44,34 +46,41 @@ func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersCount(appCtx return r0, r1 } -// FetchRequestedOfficeUsersList provides a mock function with given fields: appCtx, filters, associations, pagination, ordering -func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, error) { - ret := _m.Called(appCtx, filters, associations, pagination, ordering) +// FetchRequestedOfficeUsersList provides a mock function with given fields: appCtx, filterFuncs, pagination, ordering +func (_m *RequestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, int, error) { + ret := _m.Called(appCtx, filterFuncs, pagination, ordering) if len(ret) == 0 { panic("no return value specified for FetchRequestedOfficeUsersList") } var r0 models.OfficeUsers - var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) (models.OfficeUsers, error)); ok { - return rf(appCtx, filters, associations, pagination, ordering) + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) (models.OfficeUsers, int, error)); ok { + return rf(appCtx, filterFuncs, pagination, ordering) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) models.OfficeUsers); ok { - r0 = rf(appCtx, filters, associations, pagination, ordering) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) models.OfficeUsers); ok { + r0 = rf(appCtx, filterFuncs, pagination, ordering) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(models.OfficeUsers) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, []services.QueryFilter, services.QueryAssociations, services.Pagination, services.QueryOrder) error); ok { - r1 = rf(appCtx, filters, associations, pagination, ordering) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) int); ok { + r1 = rf(appCtx, filterFuncs, pagination, ordering) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(int) } - return r0, r1 + if rf, ok := ret.Get(2).(func(appcontext.AppContext, []func(*pop.Query), services.Pagination, services.QueryOrder) error); ok { + r2 = rf(appCtx, filterFuncs, pagination, ordering) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // NewRequestedOfficeUserListFetcher creates a new instance of RequestedOfficeUserListFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. diff --git a/pkg/services/mocks/ShipmentApprover.go b/pkg/services/mocks/ShipmentApprover.go index ede7bdcc395..55722e53f55 100644 --- a/pkg/services/mocks/ShipmentApprover.go +++ b/pkg/services/mocks/ShipmentApprover.go @@ -8,6 +8,8 @@ import ( models "github.com/transcom/mymove/pkg/models" + services "github.com/transcom/mymove/pkg/services" + uuid "github.com/gofrs/uuid" ) @@ -46,6 +48,36 @@ func (_m *ShipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipme return r0, r1 } +// ApproveShipments provides a mock function with given fields: appCtx, shipments +func (_m *ShipmentApprover) ApproveShipments(appCtx appcontext.AppContext, shipments []services.ShipmentIdWithEtag) (*[]models.MTOShipment, error) { + ret := _m.Called(appCtx, shipments) + + if len(ret) == 0 { + panic("no return value specified for ApproveShipments") + } + + var r0 *[]models.MTOShipment + var r1 error + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.ShipmentIdWithEtag) (*[]models.MTOShipment, error)); ok { + return rf(appCtx, shipments) + } + if rf, ok := ret.Get(0).(func(appcontext.AppContext, []services.ShipmentIdWithEtag) *[]models.MTOShipment); ok { + r0 = rf(appCtx, shipments) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]models.MTOShipment) + } + } + + if rf, ok := ret.Get(1).(func(appcontext.AppContext, []services.ShipmentIdWithEtag) error); ok { + r1 = rf(appCtx, shipments) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewShipmentApprover creates a new instance of ShipmentApprover. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewShipmentApprover(t interface { diff --git a/pkg/services/mocks/ShipmentRateAreaFinder.go b/pkg/services/mocks/ShipmentRateAreaFinder.go index 663a74a3cb8..594551f1295 100644 --- a/pkg/services/mocks/ShipmentRateAreaFinder.go +++ b/pkg/services/mocks/ShipmentRateAreaFinder.go @@ -16,12 +16,12 @@ type ShipmentRateAreaFinder struct { mock.Mock } -// GetPrimeMoveShipmentOconusRateArea provides a mock function with given fields: appCtx, move -func (_m *ShipmentRateAreaFinder) GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, move models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { +// GetPrimeMoveShipmentRateAreas provides a mock function with given fields: appCtx, move +func (_m *ShipmentRateAreaFinder) GetPrimeMoveShipmentRateAreas(appCtx appcontext.AppContext, move models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { ret := _m.Called(appCtx, move) if len(ret) == 0 { - panic("no return value specified for GetPrimeMoveShipmentOconusRateArea") + panic("no return value specified for GetPrimeMoveShipmentRateAreas") } var r0 *[]services.ShipmentPostalCodeRateArea diff --git a/pkg/services/mocks/TransportationOfficesFetcher.go b/pkg/services/mocks/TransportationOfficesFetcher.go index eed32753594..d0c017b3e19 100644 --- a/pkg/services/mocks/TransportationOfficesFetcher.go +++ b/pkg/services/mocks/TransportationOfficesFetcher.go @@ -46,9 +46,9 @@ func (_m *TransportationOfficesFetcher) GetAllGBLOCs(appCtx appcontext.AppContex return r0, r1 } -// GetCounselingOffices provides a mock function with given fields: appCtx, dutyLocationID -func (_m *TransportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID) (*models.TransportationOffices, error) { - ret := _m.Called(appCtx, dutyLocationID) +// GetCounselingOffices provides a mock function with given fields: appCtx, dutyLocationID, serviceMemberID +func (_m *TransportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID, serviceMemberID uuid.UUID) (*models.TransportationOffices, error) { + ret := _m.Called(appCtx, dutyLocationID, serviceMemberID) if len(ret) == 0 { panic("no return value specified for GetCounselingOffices") @@ -56,19 +56,19 @@ func (_m *TransportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.A var r0 *models.TransportationOffices var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID) (*models.TransportationOffices, error)); ok { - return rf(appCtx, dutyLocationID) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, uuid.UUID) (*models.TransportationOffices, error)); ok { + return rf(appCtx, dutyLocationID, serviceMemberID) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID) *models.TransportationOffices); ok { - r0 = rf(appCtx, dutyLocationID) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, uuid.UUID, uuid.UUID) *models.TransportationOffices); ok { + r0 = rf(appCtx, dutyLocationID, serviceMemberID) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.TransportationOffices) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID) error); ok { - r1 = rf(appCtx, dutyLocationID) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, uuid.UUID, uuid.UUID) error); ok { + r1 = rf(appCtx, dutyLocationID, serviceMemberID) } else { r1 = ret.Error(1) } @@ -106,9 +106,9 @@ func (_m *TransportationOfficesFetcher) GetTransportationOffice(appCtx appcontex return r0, r1 } -// GetTransportationOffices provides a mock function with given fields: appCtx, search, forPpm -func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) { - ret := _m.Called(appCtx, search, forPpm) +// GetTransportationOffices provides a mock function with given fields: appCtx, search, forPpm, forAdminOfficeUserReqFilter +func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) { + ret := _m.Called(appCtx, search, forPpm, forAdminOfficeUserReqFilter) if len(ret) == 0 { panic("no return value specified for GetTransportationOffices") @@ -116,19 +116,19 @@ func (_m *TransportationOfficesFetcher) GetTransportationOffices(appCtx appconte var r0 *models.TransportationOffices var r1 error - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool) (*models.TransportationOffices, error)); ok { - return rf(appCtx, search, forPpm) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool, bool) (*models.TransportationOffices, error)); ok { + return rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } - if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool) *models.TransportationOffices); ok { - r0 = rf(appCtx, search, forPpm) + if rf, ok := ret.Get(0).(func(appcontext.AppContext, string, bool, bool) *models.TransportationOffices); ok { + r0 = rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*models.TransportationOffices) } } - if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, bool) error); ok { - r1 = rf(appCtx, search, forPpm) + if rf, ok := ret.Get(1).(func(appcontext.AppContext, string, bool, bool) error); ok { + r1 = rf(appCtx, search, forPpm, forAdminOfficeUserReqFilter) } else { r1 = ret.Error(1) } diff --git a/pkg/services/move.go b/pkg/services/move.go index 0262e8da8ae..ff3abe681de 100644 --- a/pkg/services/move.go +++ b/pkg/services/move.go @@ -33,6 +33,7 @@ type MoveFetcherBulkAssignment interface { FetchMovesForBulkAssignmentCounseling(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) FetchMovesForBulkAssignmentCloseout(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) FetchMovesForBulkAssignmentTaskOrder(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) + FetchMovesForBulkAssignmentPaymentRequest(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) } //go:generate mockery --name MoveSearcher diff --git a/pkg/services/move/move_fetcher.go b/pkg/services/move/move_fetcher.go index c0ca805f7c1..e0d6aea4f8a 100644 --- a/pkg/services/move/move_fetcher.go +++ b/pkg/services/move/move_fetcher.go @@ -27,7 +27,7 @@ func (f moveFetcher) FetchMove(appCtx appcontext.AppContext, locator string, sea move := &models.Move{} query := appCtx.DB(). EagerPreload("CloseoutOffice.Address", "Contractor", "ShipmentGBLOC", "LockedByOfficeUser", "LockedByOfficeUser.TransportationOffice", "AdditionalDocuments", - "AdditionalDocuments.UserUploads"). + "AdditionalDocuments.UserUploads", "CounselingOffice"). LeftJoin("move_to_gbloc", "move_to_gbloc.move_id = moves.id"). LeftJoin("office_users", "office_users.id = moves.locked_by"). Where("locator = $1", locator) @@ -135,7 +135,8 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCounseling(appCtx INNER JOIN mto_shipments ON mto_shipments.move_id = moves.id LEFT JOIN ppm_shipments ON ppm_shipments.shipment_id = mto_shipments.id WHERE - moves.status = 'NEEDS SERVICE COUNSELING' + mto_shipments.deleted_at IS NULL + AND moves.status = 'NEEDS SERVICE COUNSELING' AND orders.gbloc = $1 AND moves.show = $2 AND moves.sc_assigned_id IS NULL @@ -223,37 +224,92 @@ func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentCloseout(appCtx ap func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentTaskOrder(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) { var moves []models.MoveWithEarliestDate - err := appCtx.DB(). - RawQuery(`SELECT - moves.id, - MIN(LEAST( - COALESCE(mto_shipments.requested_pickup_date, '9999-12-31'), - COALESCE(mto_shipments.requested_delivery_date, '9999-12-31'), - COALESCE(ppm_shipments.expected_departure_date, '9999-12-31') - )) AS earliest_date - FROM moves - INNER JOIN orders ON orders.id = moves.orders_id - INNER JOIN service_members ON orders.service_member_id = service_members.id - INNER JOIN mto_shipments ON mto_shipments.move_id = moves.id - LEFT JOIN ppm_shipments ON ppm_shipments.shipment_id = mto_shipments.id - LEFT JOIN move_to_gbloc ON move_to_gbloc.move_id = moves.id - WHERE - (moves.status IN ('APPROVALS REQUESTED', 'SUBMITTED', 'SERVICE COUNSELING COMPLETED')) - AND moves.show = $1 - AND moves.too_assigned_id IS NULL - AND (orders.orders_type NOT IN ($2, $3, $4)) - AND service_members.affiliation != 'MARINES' - AND ((mto_shipments.shipment_type != $5 AND move_to_gbloc.gbloc = $6) OR (mto_shipments.shipment_type = $7 AND orders.gbloc = $8)) - GROUP BY moves.id - ORDER BY earliest_date ASC`, - models.BoolPointer(true), - internalmessages.OrdersTypeBLUEBARK, - internalmessages.OrdersTypeWOUNDEDWARRIOR, - internalmessages.OrdersTypeSAFETY, - models.MTOShipmentTypeHHGOutOfNTS, - gbloc, - models.MTOShipmentTypeHHGOutOfNTS, - gbloc). + sqlQuery := ` + SELECT + moves.id, + MIN(LEAST( + COALESCE(mto_shipments.requested_pickup_date, '9999-12-31'), + COALESCE(mto_shipments.requested_delivery_date, '9999-12-31'), + COALESCE(ppm_shipments.expected_departure_date, '9999-12-31') + )) AS earliest_date + FROM moves + INNER JOIN orders ON orders.id = moves.orders_id + INNER JOIN service_members ON orders.service_member_id = service_members.id + INNER JOIN mto_shipments ON mto_shipments.move_id = moves.id + INNER JOIN duty_locations as origin_dl ON orders.origin_duty_location_id = origin_dl.id + LEFT JOIN ppm_shipments ON ppm_shipments.shipment_id = mto_shipments.id + LEFT JOIN move_to_gbloc ON move_to_gbloc.move_id = moves.id + WHERE + mto_shipments.deleted_at IS NULL + AND (moves.status IN ('APPROVALS REQUESTED', 'SUBMITTED', 'SERVICE COUNSELING COMPLETED')) + AND moves.show = $1 + AND moves.too_assigned_id IS NULL + AND (orders.orders_type NOT IN ($2, $3, $4)) + AND (moves.ppm_type IS NULL OR (moves.ppm_type = 'PARTIAL' or (moves.ppm_type = 'FULL' and origin_dl.provides_services_counseling = 'false'))) ` + if gbloc == "USMC" { + sqlQuery += ` + AND service_members.affiliation ILIKE 'MARINES' ` + } else { + sqlQuery += ` + AND service_members.affiliation != 'MARINES' + AND ((mto_shipments.shipment_type != 'HHG_OUTOF_NTS' AND move_to_gbloc.gbloc = '` + gbloc + `') + OR (mto_shipments.shipment_type = 'HHG_OUTOF_NTS' AND orders.gbloc = '` + gbloc + `')) ` + } + sqlQuery += ` + GROUP BY moves.id + ORDER BY earliest_date ASC` + + err := appCtx.DB().RawQuery(sqlQuery, + models.BoolPointer(true), + internalmessages.OrdersTypeBLUEBARK, + internalmessages.OrdersTypeWOUNDEDWARRIOR, + internalmessages.OrdersTypeSAFETY). + All(&moves) + + if err != nil { + return nil, fmt.Errorf("error fetching moves for office: %s with error %w", officeId, err) + } + + if len(moves) < 1 { + return nil, nil + } + + return moves, nil +} + +func (f moveFetcherBulkAssignment) FetchMovesForBulkAssignmentPaymentRequest(appCtx appcontext.AppContext, gbloc string, officeId uuid.UUID) ([]models.MoveWithEarliestDate, error) { + var moves []models.MoveWithEarliestDate + + sqlQuery := ` + SELECT + moves.id, + min(payment_requests.requested_at) AS earliest_date + FROM moves + INNER JOIN orders ON orders.id = moves.orders_id + INNER JOIN service_members ON orders.service_member_id = service_members.id + INNER JOIN payment_requests on payment_requests.move_id = moves.id + LEFT JOIN move_to_gbloc ON move_to_gbloc.move_id = moves.id + WHERE moves.show = $1 + AND (orders.orders_type NOT IN ($2, $3, $4)) + AND moves.tio_assigned_id IS NULL + AND payment_requests.status = 'PENDING' ` + if gbloc == "USMC" { + sqlQuery += ` + AND service_members.affiliation ILIKE 'MARINES' ` + } else { + sqlQuery += ` + AND service_members.affiliation != 'MARINES' + AND move_to_gbloc.gbloc = '` + gbloc + `' ` + } + sqlQuery += ` + GROUP BY moves.id + ORDER BY earliest_date ASC` + + err := appCtx.DB().RawQuery(sqlQuery, + models.BoolPointer(true), + internalmessages.OrdersTypeBLUEBARK, + internalmessages.OrdersTypeWOUNDEDWARRIOR, + internalmessages.OrdersTypeSAFETY). All(&moves) if err != nil { diff --git a/pkg/services/move/move_fetcher_test.go b/pkg/services/move/move_fetcher_test.go index e6144353c88..62b6551add5 100644 --- a/pkg/services/move/move_fetcher_test.go +++ b/pkg/services/move/move_fetcher_test.go @@ -3,6 +3,8 @@ package move import ( "time" + "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/gen/internalmessages" @@ -105,7 +107,7 @@ func (suite *MoveServiceSuite) TestMoveFetcher() { }) } -func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { +func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignmentSC() { setupTestData := func() (services.MoveFetcherBulkAssignment, models.Move, models.TransportationOffice, models.OfficeUser) { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) @@ -149,7 +151,7 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { return moveFetcher, move, transportationOffice, officeUser } - suite.Run("Returns moves that fulfill the query's criteria", func() { + suite.Run("SC - Returns moves that fulfill the query's criteria", func() { moveFetcher, _, _, officeUser := setupTestData() moves, err := moveFetcher.FetchMovesForBulkAssignmentCounseling(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) suite.FatalNoError(err) @@ -517,10 +519,13 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.Equal(1, len(moves)) suite.NotEqual(marinePPM.ID, moves[0].ID) }) +} - suite.Run("TOO: Returns moves that fulfill the query criteria", func() { +func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignmentTOO() { + setupTestData := func() (services.MoveFetcherBulkAssignment, models.TransportationOffice, models.OfficeUser) { moveFetcher := NewMoveFetcherBulkAssignment() transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ { Model: transportationOffice, @@ -552,6 +557,128 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { Type: &factory.TransportationOffices.CounselingOffice, }, }, nil) + + return moveFetcher, transportationOffice, officeUser + } + + suite.Run("TOO: Returns moves that fulfill the query's criteria", func() { + moveFetcher, _, officeUser := setupTestData() + moves, err := moveFetcher.FetchMovesForBulkAssignmentTaskOrder(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("TOO: Does not return moves with safety, bluebark, or wounded warrior order types", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeSAFETY, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeBLUEBARK, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeWOUNDEDWARRIOR, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentTaskOrder(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("TOO: Does not return moves that are already assigned", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + + assignedMove := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: officeUser, + LinkOnly: true, + Type: &factory.OfficeUsers.TOOAssignedUser, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentTaskOrder(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + + // confirm that the assigned move isn't returned + for _, move := range moves { + suite.NotEqual(move.ID, assignedMove.ID) + } + + // confirm that the rest of the details are correct + // move is SERVICE COUNSELING COMPLETED + suite.Equal(assignedMove.Status, models.MoveStatusServiceCounselingCompleted) + // GBLOC is the same + suite.Equal(*assignedMove.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(assignedMove.Show, models.BoolPointer(true)) + // Orders type isn't WW, BB, or Safety + suite.Equal(assignedMove.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("TOO: Does not return payment requests with Marines if GBLOC not USMC", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + marine := models.AffiliationMARINES factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ { @@ -574,4 +701,360 @@ func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignment() { suite.FatalNoError(err) suite.Equal(2, len(moves)) }) + + suite.Run("TOO: Only return payment requests with Marines if GBLOC is USMC", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + + marine := models.AffiliationMARINES + factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.ServiceMember{ + Affiliation: &marine, + }, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentTaskOrder(suite.AppContextForTest(), "USMC", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(1, len(moves)) + }) + +} + +func (suite *MoveServiceSuite) TestMoveFetcherBulkAssignmentTIO() { + setupTestData := func() (services.MoveFetcherBulkAssignment, models.TransportationOffice, models.OfficeUser) { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + // this move has a transportation office associated with it that matches + // the TIO's transportation office and should be found + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + move2 := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move2, + LinkOnly: true, + }, + }, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + + return moveFetcher, transportationOffice, officeUser + } + + suite.Run("TIO: Returns moves that fulfill the query criteria", func() { + moveFetcher, _, officeUser := setupTestData() + moves, err := moveFetcher.FetchMovesForBulkAssignmentPaymentRequest(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("Does not return moves that are already assigned", func() { + moveFetcher := NewMoveFetcherBulkAssignment() + transportationOffice := factory.BuildTransportationOffice(suite.DB(), nil, nil) + + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: officeUser, + LinkOnly: true, + Type: &factory.OfficeUsers.TIOAssignedUser, + }, + }, nil) + assignedPaymentRequest := factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentPaymentRequest(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + + // confirm that the assigned move isn't returned + for _, move := range moves { + suite.NotEqual(move.ID, assignedPaymentRequest.ID) + } + + // confirm that the rest of the details are correct + // move is APPROVALS REQUESTED STATUS + suite.Equal(assignedPaymentRequest.Status, models.PaymentRequestStatusPending) + // GBLOC is the same + suite.Equal(*move.Orders.OriginDutyLocationGBLOC, officeUser.TransportationOffice.Gbloc) + // Show is true + suite.Equal(move.Show, models.BoolPointer(true)) + // Orders type isn't WW, BB, or Safety + suite.Equal(move.Orders.OrdersType, internalmessages.OrdersTypePERMANENTCHANGEOFSTATION) + }) + + suite.Run("TIO: Does not return moves with safety, bluebark, or wounded warrior order types", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + moveSafety := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeSAFETY, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: moveSafety, + LinkOnly: true, + }, + }, nil) + + moveBB := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeBLUEBARK, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: moveBB, + LinkOnly: true, + }, + }, nil) + + moveWW := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OrdersType: internalmessages.OrdersTypeWOUNDEDWARRIOR, + }, + }, + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: moveWW, + LinkOnly: true, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentPaymentRequest(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("TIO: Does not return payment requests with Marines if GBLOC not USMC", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + + marine := models.AffiliationMARINES + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVALSREQUESTED, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.ServiceMember{ + Affiliation: &marine, + }, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentPaymentRequest(suite.AppContextForTest(), "KKFA", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(2, len(moves)) + }) + + suite.Run("TIO: Only return payment requests with Marines if GBLOC is USMC", func() { + moveFetcher, transportationOffice, officeUser := setupTestData() + + marine := models.AffiliationMARINES + move := factory.BuildMoveWithShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusServiceCounselingCompleted, + }, + }, + { + Model: transportationOffice, + LinkOnly: true, + Type: &factory.TransportationOffices.CounselingOffice, + }, + { + Model: models.ServiceMember{ + Affiliation: &marine, + }, + }, + }, nil) + factory.BuildPaymentRequest(suite.DB(), []factory.Customization{ + { + Model: models.PaymentRequest{ + ID: uuid.Must(uuid.NewV4()), + IsFinal: false, + Status: models.PaymentRequestStatusPending, + RejectionReason: nil, + }, + }, + { + Model: move, + LinkOnly: true, + }, + }, nil) + + moves, err := moveFetcher.FetchMovesForBulkAssignmentPaymentRequest(suite.AppContextForTest(), "USMC", officeUser.TransportationOffice.ID) + suite.FatalNoError(err) + suite.Equal(1, len(moves)) + }) } diff --git a/pkg/services/move_task_order.go b/pkg/services/move_task_order.go index 717d2b7c566..9f0d3a9f01e 100644 --- a/pkg/services/move_task_order.go +++ b/pkg/services/move_task_order.go @@ -57,13 +57,14 @@ type MoveTaskOrderFetcher interface { // //go:generate mockery --name MoveTaskOrderUpdater type MoveTaskOrderUpdater interface { - MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, includeServiceCodeMS bool, includeServiceCodeCS bool) (*models.Move, error) + ApproveMoveAndCreateServiceItems(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, includeServiceCodeMS bool, includeServiceCodeCS bool) (*models.Move, error) UpdatePostCounselingInfo(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string) (*models.Move, error) UpdateStatusServiceCounselingCompleted(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string) (*models.Move, error) UpdateReviewedBillableWeightsAt(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string) (*models.Move, error) UpdateTIORemarks(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, remarks string) (*models.Move, error) ShowHide(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, show *bool) (*models.Move, error) UpdatePPMType(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID) (*models.Move, error) + MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID) (*models.Move, bool, error) } // MoveTaskOrderChecker is the service object interface for checking if a MoveTaskOrder is in a certain state diff --git a/pkg/services/move_task_order/move_task_order_fetcher.go b/pkg/services/move_task_order/move_task_order_fetcher.go index 50fc813bd06..f93bbf51681 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher.go +++ b/pkg/services/move_task_order/move_task_order_fetcher.go @@ -14,6 +14,7 @@ import ( "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/apperror" "github.com/transcom/mymove/pkg/cli" + "github.com/transcom/mymove/pkg/db/utilities" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/featureflag" @@ -158,7 +159,6 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s "MTOShipments.SecondaryPickupAddress.Country", "MTOShipments.TertiaryDeliveryAddress.Country", "MTOShipments.TertiaryPickupAddress.Country", - "MTOShipments.MTOAgents", "MTOShipments.SITDurationUpdates", "MTOShipments.StorageFacility", "MTOShipments.StorageFacility.Address", @@ -199,6 +199,20 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s } } + for i := range mto.MTOShipments { + var nonDeletedAgents models.MTOAgents + loadErr := appCtx.DB(). + Scope(utilities.ExcludeDeletedScope()). + Where("mto_shipment_id = ?", mto.MTOShipments[i].ID). + All(&nonDeletedAgents) + + if loadErr != nil { + return &models.Move{}, apperror.NewQueryError("MTOAgents", loadErr, "") + } + + mto.MTOShipments[i].MTOAgents = nonDeletedAgents + } + // Now that we have the move and order, construct the allotment (hhg allowance) // Only fetch if grade is not nil if mto.Orders.Grade != nil { @@ -338,12 +352,11 @@ func (f moveTaskOrderFetcher) FetchMoveTaskOrder(appCtx appcontext.AppContext, s if loadErr != nil { return &models.Move{}, apperror.NewQueryError("CustomerContacts", loadErr, "") } - } else if serviceItem.ReService.Code == models.ReServiceCodeICRT || // use address.isOconus to get 'market' value for intl crating - serviceItem.ReService.Code == models.ReServiceCodeIUCRT { - loadErr := appCtx.DB().Load(&mto.MTOServiceItems[i], "MTOShipment.PickupAddress", "MTOShipment.DestinationAddress") - if loadErr != nil { - return &models.Move{}, apperror.NewQueryError("MTOShipment.PickupAddress, MTOShipment.DestinationAddress", loadErr, "") - } + } + + loadErr := appCtx.DB().Load(&mto.MTOServiceItems[i], "MTOShipment.PickupAddress", "MTOShipment.DestinationAddress") + if loadErr != nil { + return &models.Move{}, apperror.NewQueryError("MTOShipment", loadErr, "") } loadedServiceItems = append(loadedServiceItems, mto.MTOServiceItems[i]) diff --git a/pkg/services/move_task_order/move_task_order_fetcher_test.go b/pkg/services/move_task_order/move_task_order_fetcher_test.go index 07565525901..3cf793bc3ba 100644 --- a/pkg/services/move_task_order/move_task_order_fetcher_test.go +++ b/pkg/services/move_task_order/move_task_order_fetcher_test.go @@ -569,6 +569,75 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderFetcher() { suite.True(found, "Expected service item ReServiceCodePODFSC") }) + suite.Run("Success - Move contains only deleted MTOAgents", func() { + move := factory.BuildMove(suite.DB(), nil, nil) + + factory.BuildMTOAgent(suite.DB(), []factory.Customization{ + {Model: models.MTOAgent{ + MTOAgentType: models.MTOAgentReceiving, + DeletedAt: models.TimePointer(time.Now()), + }}, + {Model: move, LinkOnly: true}, + }, nil) + + searchParams := services.MoveTaskOrderFetcherParams{ + MoveTaskOrderID: move.ID, + } + + actualMTO, err := mtoFetcher.FetchMoveTaskOrder(suite.AppContextForTest(), &searchParams) + suite.NoError(err) + + suite.Equal(move.ID, actualMTO.ID) + suite.Len(actualMTO.MTOShipments[0].MTOAgents, 0, "Expected no active agents since all are deleted") + }) + + suite.Run("Success - Move contains one MTOAgent", func() { + move := factory.BuildMove(suite.DB(), nil, nil) + + shipment := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + {Model: move, + LinkOnly: true}, + }, nil) + + factory.BuildMTOAgent(suite.DB(), []factory.Customization{ + {Model: models.MTOAgent{ + MTOAgentType: models.MTOAgentReceiving, + DeletedAt: models.TimePointer(time.Now()), + }}, + {Model: move, LinkOnly: true}, + {Model: shipment, LinkOnly: true}, + }, nil) + + activeAgent := factory.BuildMTOAgent(suite.DB(), []factory.Customization{ + {Model: models.MTOAgent{ + FirstName: models.StringPointer("John"), + LastName: models.StringPointer("Doe"), + Email: models.StringPointer("John.doe@example.com"), + Phone: models.StringPointer("222-222-2222"), + MTOAgentType: models.MTOAgentReleasing, + }}, + {Model: move, LinkOnly: true}, + {Model: shipment, LinkOnly: true}, + }, nil) + + searchParams := services.MoveTaskOrderFetcherParams{ + MoveTaskOrderID: move.ID, + } + + actualMTO, err := mtoFetcher.FetchMoveTaskOrder(suite.AppContextForTest(), &searchParams) + suite.NoError(err) + + suite.Equal(move.ID, actualMTO.ID) + suite.Len(actualMTO.MTOShipments[0].MTOAgents, 1, "Expected only one active agent in the result") + + activeAgentReturned := actualMTO.MTOShipments[0].MTOAgents[0] + suite.Equal(activeAgent.FirstName, activeAgentReturned.FirstName, "First names should match") + suite.Equal(activeAgent.LastName, activeAgentReturned.LastName, "Last names should match") + suite.Equal(activeAgent.Email, activeAgentReturned.Email, "Emails should match") + suite.Equal(activeAgent.Phone, activeAgentReturned.Phone, "Phone numbers should match") + suite.Equal(activeAgent.MTOAgentType, activeAgentReturned.MTOAgentType, "Agent types should match") + }) + } func (suite *MoveTaskOrderServiceSuite) TestGetMoveTaskOrderFetcher() { diff --git a/pkg/services/move_task_order/move_task_order_updater.go b/pkg/services/move_task_order/move_task_order_updater.go index d0cd55aed37..463c5e33a37 100644 --- a/pkg/services/move_task_order/move_task_order_updater.go +++ b/pkg/services/move_task_order/move_task_order_updater.go @@ -239,11 +239,11 @@ func (o moveTaskOrderUpdater) UpdateTIORemarks(appCtx appcontext.AppContext, mov return move, nil } -// MakeAvailableToPrime approves a Move, makes it available to prime, and +// ApproveMoveAndCreateServiceItems approves a Move and // creates Move-level service items (counseling and move management) if the // TOO selected them. If the move received service counseling, the counseling // service item will automatically be created without the TOO having to select it. -func (o *moveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, +func (o *moveTaskOrderUpdater) ApproveMoveAndCreateServiceItems(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID, eTag string, includeServiceCodeMS bool, includeServiceCodeCS bool) (*models.Move, error) { searchParams := services.MoveTaskOrderFetcherParams{ @@ -263,14 +263,9 @@ func (o *moveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext //When approving a shipment - remove the assigned TOO user move.TOOAssignedID = nil - // If the move is already been made available to prime, we will not need to approve and update the move, - // just the provided service items. updateMove := false - if move.AvailableToPrimeAt == nil { + if move.ApprovedAt == nil { updateMove = true - now := time.Now() - move.AvailableToPrimeAt = &now - err = o.moveRouter.Approve(appCtx, move) if err != nil { return &models.Move{}, apperror.NewConflictError(move.ID, err.Error()) @@ -308,6 +303,42 @@ func (o *moveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext return move, nil } +// MakeAvailableToPrime makes the move available to prime +func (o *moveTaskOrderUpdater) MakeAvailableToPrime(appCtx appcontext.AppContext, moveTaskOrderID uuid.UUID) (*models.Move, bool, error) { + var move *models.Move + var wasMadeAvailableToPrime = false + + transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { + searchParams := services.MoveTaskOrderFetcherParams{ + IncludeHidden: false, + MoveTaskOrderID: moveTaskOrderID, + } + var err error + move, err = o.FetchMoveTaskOrder(txnAppCtx, &searchParams) + if err != nil { + return err + } + + if move.AvailableToPrimeAt == nil { + now := time.Now() + move.AvailableToPrimeAt = &now + + err = o.updateMove(txnAppCtx, move, order.CheckRequiredFields()) + if err != nil { + return err + } + wasMadeAvailableToPrime = true + } + return nil + }) + + if transactionError != nil { + return &models.Move{}, false, transactionError + } + + return move, wasMadeAvailableToPrime, nil +} + func (o *moveTaskOrderUpdater) updateMove(appCtx appcontext.AppContext, move *models.Move, checks ...order.Validator) error { if verr := order.ValidateOrder(&move.Orders, checks...); verr != nil { return verr diff --git a/pkg/services/move_task_order/move_task_order_updater_test.go b/pkg/services/move_task_order/move_task_order_updater_test.go index b6368248eb0..8795b151aae 100644 --- a/pkg/services/move_task_order/move_task_order_updater_test.go +++ b/pkg/services/move_task_order/move_task_order_updater_test.go @@ -88,7 +88,7 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_UpdateStatusSer moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator, ) - suite.Run("Makes move available to Prime and Removes assigned TOO office user", func() { + suite.Run("Completes counseling and removes assigned SC office user", func() { session := suite.AppContextWithSessionForTest(&auth.Session{ ApplicationName: auth.OfficeApp, OfficeUserID: uuid.Must(uuid.NewV4()), @@ -697,7 +697,7 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_ShowHide() { }) } -func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableToPrime() { +func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_ApproveMoveAndCreateServiceItems() { ppmEstimator := &mocks.PPMEstimator{} setupTestData := func() models.OfficeUser { @@ -791,13 +791,12 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo eTag := etag.GenerateEtag(move.UpdatedAt) fetchedMove := models.Move{} - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, true, true) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, true, true) mockserviceItemCreator.AssertNumberOfCalls(suite.T(), "CreateMTOServiceItem", 0) suite.Error(err) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.Nil(fetchedMove.AvailableToPrimeAt) suite.Nil(fetchedMove.ApprovedAt) }) @@ -810,14 +809,14 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo move := factory.BuildSubmittedMove(suite.DB(), nil, nil) eTag := etag.GenerateEtag(time.Now()) - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, true, true) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, true, true) mockserviceItemCreator.AssertNumberOfCalls(suite.T(), "CreateMTOServiceItem", 0) suite.Error(err) suite.IsType(apperror.PreconditionFailedError{}, err) }) - suite.Run("Makes move available to Prime and creates Move management and Service counseling service items when both are specified", func() { + suite.Run("Approves a move and creates Move management and Service counseling service items when both are specified", func() { queryBuilder := query.NewQueryBuilder() moveRouter := moverouter.NewMoveRouter() planner := &routemocks.Planner{} @@ -835,13 +834,11 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo fetchedMove := models.Move{} var serviceItems models.MTOServiceItems - suite.Nil(move.AvailableToPrimeAt) suite.Nil(move.ApprovedAt) - updatedMove, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, true, true) + updatedMove, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, true, true) suite.NoError(err) - suite.NotNil(updatedMove.AvailableToPrimeAt) suite.NotNil(updatedMove.ApprovedAt) suite.Equal(models.MoveStatusAPPROVED, updatedMove.Status) err = suite.DB().Eager("ReService").Where("move_id = ?", move.ID).All(&serviceItems) @@ -851,11 +848,10 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo suite.True(suite.containsServiceCode(serviceItems, models.ReServiceCodeCS), fmt.Sprintf("Expected to find reServiceCode, %s, in array.", models.ReServiceCodeCS)) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.NotNil(fetchedMove.AvailableToPrimeAt) suite.NotNil(fetchedMove.ApprovedAt) suite.Equal(models.MoveStatusAPPROVED, fetchedMove.Status) }) - suite.Run("Makes move available to Prime and Removes assigned TOO office user", func() { + suite.Run("Approves a move and removes assigned TOO office user", func() { queryBuilder := query.NewQueryBuilder() moveRouter := moverouter.NewMoveRouter() planner := &routemocks.Planner{} @@ -876,13 +872,13 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo serviceItemCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) eTag := etag.GenerateEtag(move.UpdatedAt) - updatedMove, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, false, false) + updatedMove, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, false, false) suite.NoError(err) suite.Nil(updatedMove.TOOAssignedID) }) - suite.Run("Makes move available to Prime and only creates Move management when it's the only one specified", func() { + suite.Run("Approves a move and only creates Move management when it's the only one specified", func() { queryBuilder := query.NewQueryBuilder() moveRouter := moverouter.NewMoveRouter() planner := &routemocks.Planner{} @@ -900,15 +896,13 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo fetchedMove := models.Move{} var serviceItems models.MTOServiceItems - suite.Nil(move.AvailableToPrimeAt) suite.Nil(move.ApprovedAt) - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, true, false) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, true, false) suite.NoError(err) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.NotNil(fetchedMove.AvailableToPrimeAt) suite.NotNil(fetchedMove.ApprovedAt) err = suite.DB().Eager("ReService").Where("move_id = ?", move.ID).All(&serviceItems) suite.NoError(err) @@ -917,7 +911,7 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo suite.False(suite.containsServiceCode(serviceItems, models.ReServiceCodeCS), fmt.Sprintf("Expected to find reServiceCode, %s, in array.", models.ReServiceCodeCS)) }) - suite.Run("Makes move available to Prime and only creates CS service item when it's the only one specified", func() { + suite.Run("Approves a move and only creates CS service item when it's the only one specified", func() { queryBuilder := query.NewQueryBuilder() moveRouter := moverouter.NewMoveRouter() planner := &routemocks.Planner{} @@ -929,15 +923,13 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo fetchedMove := models.Move{} var serviceItems models.MTOServiceItems - suite.Nil(move.AvailableToPrimeAt) suite.Nil(move.ApprovedAt) - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, false, true) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, false, true) suite.NoError(err) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.NotNil(fetchedMove.AvailableToPrimeAt) suite.NotNil(fetchedMove.ApprovedAt) err = suite.DB().Eager("ReService").Where("move_id = ?", move.ID).All(&serviceItems) suite.NoError(err) @@ -956,20 +948,18 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo eTag := etag.GenerateEtag(move.UpdatedAt) fetchedMove := models.Move{} - suite.Nil(move.AvailableToPrimeAt) suite.Nil(move.ApprovedAt) - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, false, false) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, false, false) mockserviceItemCreator.AssertNumberOfCalls(suite.T(), "CreateMTOServiceItem", 0) suite.NoError(err) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.NotNil(fetchedMove.AvailableToPrimeAt) suite.NotNil(fetchedMove.ApprovedAt) }) - suite.Run("Does not make move available to prime if Order is missing required fields", func() { + suite.Run("Does not approve a move if Order is missing required fields", func() { mockserviceItemCreator := &mocks.MTOServiceItemCreator{} queryBuilder := query.NewQueryBuilder() moveRouter := moverouter.NewMoveRouter() @@ -985,18 +975,134 @@ func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableTo eTag := etag.GenerateEtag(move.UpdatedAt) fetchedMove := models.Move{} - _, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID, eTag, true, true) + _, err := mtoUpdater.ApproveMoveAndCreateServiceItems(suite.AppContextForTest(), move.ID, eTag, true, true) mockserviceItemCreator.AssertNumberOfCalls(suite.T(), "CreateMTOServiceItem", 0) suite.Error(err) suite.IsType(apperror.InvalidInputError{}, err) err = suite.DB().Find(&fetchedMove, move.ID) suite.NoError(err) - suite.Nil(fetchedMove.AvailableToPrimeAt) suite.Nil(fetchedMove.ApprovedAt) }) } +func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_MakeAvailableToPrime() { + ppmEstimator := &mocks.PPMEstimator{} + + setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { + mockCreator := &mocks.SignedCertificationCreator{} + + mockCreator.On( + "CreateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + ).Return(returnValue...) + + return mockCreator + } + + setUpSignedCertificationUpdaterMock := func(returnValue ...interface{}) services.SignedCertificationUpdater { + mockUpdater := &mocks.SignedCertificationUpdater{} + + mockUpdater.On( + "UpdateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + mock.AnythingOfType("string"), + ).Return(returnValue...) + + return mockUpdater + } + + setupPricerData := func() { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + + startDate := time.Date(2020, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2020, time.December, 31, 12, 0, 0, 0, time.UTC) + contractYear := testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + + service := factory.FetchReServiceByCode(suite.DB(), "MS") + msTaskOrderFee := models.ReTaskOrderFee{ + ContractYearID: contractYear.ID, + ServiceID: service.ID, + PriceCents: 90000, + } + suite.MustSave(&msTaskOrderFee) + + service = factory.FetchReServiceByCode(suite.DB(), "CS") + csTaskOrderFee := models.ReTaskOrderFee{ + ContractYearID: contractYear.ID, + ServiceID: service.ID, + PriceCents: 90000, + } + suite.MustSave(&csTaskOrderFee) + } + + suite.PreloadData(setupPricerData) + + suite.Run("Successfully makes move available to Prime", func() { + mockserviceItemCreator := &mocks.MTOServiceItemCreator{} + queryBuilder := query.NewQueryBuilder() + moveRouter := moverouter.NewMoveRouter() + mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, mockserviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) + move := factory.BuildMove(suite.DB(), nil, nil) + + result, wasUpdated, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID) + + suite.NoError(err) + suite.NotNil(result) + suite.True(wasUpdated) + suite.NotNil(result.AvailableToPrimeAt) + }) + + suite.Run("Does not update move that is already available to Prime", func() { + mockserviceItemCreator := &mocks.MTOServiceItemCreator{} + queryBuilder := query.NewQueryBuilder() + moveRouter := moverouter.NewMoveRouter() + mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, mockserviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) + + now := time.Now() + + move := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + AvailableToPrimeAt: &now, + }, + }, + }, nil) + + result, wasUpdated, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), move.ID) + + suite.NoError(err) + suite.NotNil(result) + suite.False(wasUpdated) + suite.WithinDuration(now.UTC(), result.AvailableToPrimeAt.UTC(), time.Second) + }) + + suite.Run("Returns error if move is not found", func() { + mockserviceItemCreator := &mocks.MTOServiceItemCreator{} + queryBuilder := query.NewQueryBuilder() + moveRouter := moverouter.NewMoveRouter() + mtoUpdater := mt.NewMoveTaskOrderUpdater(queryBuilder, mockserviceItemCreator, moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator) + moveID := uuid.Must(uuid.NewV4()) + + _, wasUpdated, err := mtoUpdater.MakeAvailableToPrime(suite.AppContextForTest(), moveID) + + suite.Error(err) + suite.False(wasUpdated) + suite.IsType(apperror.NotFoundError{}, err) + }) +} + func (suite *MoveTaskOrderServiceSuite) TestMoveTaskOrderUpdater_BillableWeightsReviewedAt() { ppmEstimator := &mocks.PPMEstimator{} setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { diff --git a/pkg/services/mto_service_item/mto_service_item_creator.go b/pkg/services/mto_service_item/mto_service_item_creator.go index 88ce0cde8c2..e71a4d5249d 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator.go +++ b/pkg/services/mto_service_item/mto_service_item_creator.go @@ -375,7 +375,7 @@ func (o *mtoServiceItemCreator) CreateMTOServiceItem(appCtx appcontext.AppContex err := o.checkDuplicateServiceCodes(appCtx, serviceItem) if err != nil { appCtx.Logger().Error(fmt.Sprintf("Error trying to create a duplicate MS service item for move ID: %s", move.ID), zap.Error(err)) - return nil, nil, err + return &createdServiceItems, nil, nil } } diff --git a/pkg/services/mto_service_item/mto_service_item_creator_test.go b/pkg/services/mto_service_item/mto_service_item_creator_test.go index 619be14b026..fe102abe17a 100644 --- a/pkg/services/mto_service_item/mto_service_item_creator_test.go +++ b/pkg/services/mto_service_item/mto_service_item_creator_test.go @@ -503,7 +503,7 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItem() { // Under test: CreateMTOServiceItem function // Set up: Then create service items for CS or MS. Then try to create again. // Expected outcome: - // Fail, MS cannot be created if there is one already created for the move. + // Return empty MTOServiceItems and continue, MS cannot be created if there is one already created for the move. contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) @@ -555,10 +555,9 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItem() { createdServiceItemsMSDupe, _, err := creator.CreateMTOServiceItem(suite.AppContextForTest(), &serviceItemMS) - fakeMTOShipmentRouterErr := apperror.NewConflictError(serviceItemMS.ID, fmt.Sprintf("for creating a service item. A service item with reServiceCode %s already exists for this move and/or shipment.", serviceItemMS.ReService.Code)) - - suite.Nil(createdServiceItemsMSDupe) - suite.Equal(fakeMTOShipmentRouterErr.Error(), err.Error()) + suite.Nil(err) + suite.NotNil(createdServiceItemsMSDupe) + suite.Equal(*createdServiceItemsMSDupe, models.MTOServiceItems(nil)) }) // Should not be able to create CS or MS service items unless a shipment within the move has a requested pickup date @@ -820,7 +819,7 @@ func (suite *MTOServiceItemServiceSuite) TestCreateMTOServiceItem() { }) // If the service item we're trying to create is shuttle service and there is no estimated weight, it fails. - suite.Run("MTOServiceItemShuttle no prime weight is okay", func() { + suite.Run("MTOServiceItemDomesticShuttle no prime weight is okay", func() { // TESTCASE SCENARIO // Under test: CreateMTOServiceItem function // Set up: Create DDSHUT service item on a shipment without estimated weight diff --git a/pkg/services/mto_service_item/mto_service_item_validators.go b/pkg/services/mto_service_item/mto_service_item_validators.go index 25a6768c335..989c892c20f 100644 --- a/pkg/services/mto_service_item/mto_service_item_validators.go +++ b/pkg/services/mto_service_item/mto_service_item_validators.go @@ -344,13 +344,10 @@ func (v *updateMTOServiceItemData) checkOldServiceItemStatus(_ appcontext.AppCon } if slices.Contains(allAccessorialServiceItemsToCheck, serviceItemData.oldServiceItem.ReService.Code) { + invalidFieldChange := false if serviceItemData.oldServiceItem.Status == models.MTOServiceItemStatusRejected { return nil - } else if serviceItemData.oldServiceItem.Status == models.MTOServiceItemStatusApproved { - - invalidFieldChange := false - // Fields that are not allowed to change when status is approved - + } else if serviceItemData.oldServiceItem.Status == models.MTOServiceItemStatusSubmitted || serviceItemData.oldServiceItem.Status == models.MTOServiceItemStatusApproved { if serviceItemData.updatedServiceItem.ReService.Code.String() != "" && serviceItemData.updatedServiceItem.ReService.Code.String() != serviceItemData.oldServiceItem.ReService.Code.String() { invalidFieldChange = true } @@ -359,6 +356,10 @@ func (v *updateMTOServiceItemData) checkOldServiceItemStatus(_ appcontext.AppCon invalidFieldChange = true } + if serviceItemData.updatedServiceItem.EstimatedWeight != nil { + invalidFieldChange = true + } + if serviceItemData.updatedServiceItem.RequestedApprovalsRequestedStatus != nil { invalidFieldChange = true } @@ -366,10 +367,10 @@ func (v *updateMTOServiceItemData) checkOldServiceItemStatus(_ appcontext.AppCon if invalidFieldChange { return apperror.NewConflictError(serviceItemData.oldServiceItem.ID, "- one or more fields is not allowed to be updated when the shuttle service item has an approved status.") + } else { + return nil } - return apperror.NewConflictError(serviceItemData.oldServiceItem.ID, - "- unknown field or fields attempting to be updated.") } else { return apperror.NewConflictError(serviceItemData.oldServiceItem.ID, "- this shuttle service item cannot be updated because the status is not in an editable state.") diff --git a/pkg/services/mto_shipment.go b/pkg/services/mto_shipment.go index 187d290cb9f..fab8507e43d 100644 --- a/pkg/services/mto_shipment.go +++ b/pkg/services/mto_shipment.go @@ -49,11 +49,17 @@ type ShipmentDeleter interface { DeleteShipment(appCtx appcontext.AppContext, shipmentID uuid.UUID) (uuid.UUID, error) } +type ShipmentIdWithEtag struct { + ShipmentID uuid.UUID + ETag string +} + // ShipmentApprover is the service object interface for approving a shipment // //go:generate mockery --name ShipmentApprover type ShipmentApprover interface { ApproveShipment(appCtx appcontext.AppContext, shipmentID uuid.UUID, eTag string) (*models.MTOShipment, error) + ApproveShipments(appCtx appcontext.AppContext, shipments []ShipmentIdWithEtag) (*[]models.MTOShipment, error) } // ShipmentDiversionRequester is the service object interface for requesting a shipment diversion @@ -163,5 +169,5 @@ type ShipmentPostalCodeRateArea struct { // //go:generate mockery --name ShipmentRateAreaFinder type ShipmentRateAreaFinder interface { - GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, move models.Move) (*[]ShipmentPostalCodeRateArea, error) + GetPrimeMoveShipmentRateAreas(appCtx appcontext.AppContext, move models.Move) (*[]ShipmentPostalCodeRateArea, error) } diff --git a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go index bbc290dba9e..ed7381c60a9 100644 --- a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go +++ b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher.go @@ -22,7 +22,7 @@ func NewMTOShipmentRateAreaFetcher() services.ShipmentRateAreaFinder { return &mtoShipmentRateAreaFetcher{} } -func (f mtoShipmentRateAreaFetcher) GetPrimeMoveShipmentOconusRateArea(appCtx appcontext.AppContext, moveTaskOrder models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { +func (f mtoShipmentRateAreaFetcher) GetPrimeMoveShipmentRateAreas(appCtx appcontext.AppContext, moveTaskOrder models.Move) (*[]services.ShipmentPostalCodeRateArea, error) { if moveTaskOrder.AvailableToPrimeAt == nil { return nil, apperror.NewUnprocessableEntityError("Move not available to the Prime, unable to retrieve move shipment oconus rateArea") } @@ -33,53 +33,87 @@ func (f mtoShipmentRateAreaFetcher) GetPrimeMoveShipmentOconusRateArea(appCtx ap } // build set of postalCodes to fetch rateArea for - var postalCodes = make([]string, 0) + var oconusPostalCodes = make([]string, 0) + var conusPostalCodes = make([]string, 0) for _, shipment := range moveTaskOrder.MTOShipments { if shipment.PickupAddress != nil { - if !slices.Contains(postalCodes, shipment.PickupAddress.PostalCode) { - postalCodes = append(postalCodes, shipment.PickupAddress.PostalCode) + if !slices.Contains(oconusPostalCodes, shipment.PickupAddress.PostalCode) && + shipment.PickupAddress.IsOconus != nil && *shipment.PickupAddress.IsOconus { + oconusPostalCodes = append(oconusPostalCodes, shipment.PickupAddress.PostalCode) + } else if !slices.Contains(conusPostalCodes, shipment.PickupAddress.PostalCode) { + conusPostalCodes = append(conusPostalCodes, shipment.PickupAddress.PostalCode) } } if shipment.DestinationAddress != nil { - if !slices.Contains(postalCodes, shipment.DestinationAddress.PostalCode) { - postalCodes = append(postalCodes, shipment.DestinationAddress.PostalCode) + if !slices.Contains(oconusPostalCodes, shipment.DestinationAddress.PostalCode) && + shipment.DestinationAddress.IsOconus != nil && *shipment.DestinationAddress.IsOconus { + oconusPostalCodes = append(oconusPostalCodes, shipment.DestinationAddress.PostalCode) + } else if !slices.Contains(conusPostalCodes, shipment.DestinationAddress.PostalCode) { + conusPostalCodes = append(conusPostalCodes, shipment.DestinationAddress.PostalCode) } } if shipment.PPMShipment != nil { if shipment.PPMShipment.PickupAddress != nil { - if !slices.Contains(postalCodes, shipment.PPMShipment.PickupAddress.PostalCode) { - postalCodes = append(postalCodes, shipment.PPMShipment.PickupAddress.PostalCode) + if !slices.Contains(oconusPostalCodes, shipment.PPMShipment.PickupAddress.PostalCode) && + shipment.PPMShipment.PickupAddress.IsOconus != nil && *shipment.PPMShipment.PickupAddress.IsOconus { + oconusPostalCodes = append(oconusPostalCodes, shipment.PPMShipment.PickupAddress.PostalCode) + } else if !slices.Contains(conusPostalCodes, shipment.PPMShipment.PickupAddress.PostalCode) { + conusPostalCodes = append(conusPostalCodes, shipment.PPMShipment.PickupAddress.PostalCode) } } if shipment.PPMShipment.DestinationAddress != nil { - if !slices.Contains(postalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) { - postalCodes = append(postalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) + if !slices.Contains(oconusPostalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) && + shipment.PPMShipment.DestinationAddress.IsOconus != nil && *shipment.PPMShipment.DestinationAddress.IsOconus { + oconusPostalCodes = append(oconusPostalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) + } else if !slices.Contains(conusPostalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) { + conusPostalCodes = append(conusPostalCodes, shipment.PPMShipment.DestinationAddress.PostalCode) } } } } - ra, err := fetchRateArea(appCtx, contract.ID, postalCodes) + ora, err := fetchOconusRateAreas(appCtx, contract.ID, oconusPostalCodes) if err != nil { return nil, err } - return ra, nil + cra, err := fetchConusRateAreas(appCtx, contract.ID, conusPostalCodes) + if err != nil { + return nil, err + } + + ra := append(*ora, *cra...) + return &ra, nil +} + +func fetchOconusRateAreas(appCtx appcontext.AppContext, contractId uuid.UUID, postalCodes []string) (*[]services.ShipmentPostalCodeRateArea, error) { + var rateAreasMap = make([]services.ShipmentPostalCodeRateArea, 0) + for _, postalCode := range postalCodes { + ra, err := fetchOconusRateAreaByPostalCode(appCtx, contractId, postalCode) + if err != nil { + if err != sql.ErrNoRows { + return nil, apperror.NewQueryError("GetRateArea", err, fmt.Sprintf("error retrieving rateArea for contractId:%s, postalCode:%s", contractId, postalCode)) + } + } else { + rateAreasMap = append(rateAreasMap, services.ShipmentPostalCodeRateArea{PostalCode: postalCode, RateArea: ra}) + } + } + return &rateAreasMap, nil } -func fetchRateArea(appCtx appcontext.AppContext, contractId uuid.UUID, postalCode []string) (*[]services.ShipmentPostalCodeRateArea, error) { - var rateArea = make([]services.ShipmentPostalCodeRateArea, 0) - for _, code := range postalCode { - ra, err := fetchOconusRateAreaByPostalCode(appCtx, contractId, code) +func fetchConusRateAreas(appCtx appcontext.AppContext, contractId uuid.UUID, postalCodes []string) (*[]services.ShipmentPostalCodeRateArea, error) { + var rateAreasMap = make([]services.ShipmentPostalCodeRateArea, 0) + for _, postalCode := range postalCodes { + ra, err := models.FetchConusRateAreaByPostalCode(appCtx.DB(), postalCode, contractId) if err != nil { if err != sql.ErrNoRows { - return nil, apperror.NewQueryError("GetRateArea", err, fmt.Sprintf("error retrieving rateArea for contractId:%s, postalCode:%s", contractId, code)) + return nil, apperror.NewQueryError("GetRateArea", err, fmt.Sprintf("error retrieving rateArea for contractId:%s, postalCode:%s", contractId, postalCode)) } } else { - rateArea = append(rateArea, services.ShipmentPostalCodeRateArea{PostalCode: code, RateArea: ra}) + rateAreasMap = append(rateAreasMap, services.ShipmentPostalCodeRateArea{PostalCode: postalCode, RateArea: ra}) } } - return &rateArea, nil + return &rateAreasMap, nil } func fetchOconusRateAreaByPostalCode(appCtx appcontext.AppContext, contractId uuid.UUID, postalCode string) (*models.ReRateArea, error) { diff --git a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go index 976e03ac2e1..e94fea2c4a1 100644 --- a/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go +++ b/pkg/services/mto_shipment/mto_shipment_rate_area_fetcher_test.go @@ -19,6 +19,9 @@ const testContractName = "Test Contract" const fairbanksAlaskaPostalCode = "99716" const anchorageAlaskaPostalCode = "99521" const wasillaAlaskaPostalCode = "99652" +const beverlyHillsCAPostalCode = "90210" +const sanDiegoCAPostalCode = "92075" +const brooklynNYPostalCode = "11220" func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { shipmentRateAreaFetcher := NewMTOShipmentRateAreaFetcher() @@ -34,12 +37,14 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { City: "Fairbanks", State: "AK", PostalCode: fairbanksAlaskaPostalCode, + IsOconus: models.BoolPointer(true), }, DestinationAddress: &models.Address{ StreetAddress1: "123 Main St", City: "Anchorage", State: "AK", PostalCode: anchorageAlaskaPostalCode, + IsOconus: models.BoolPointer(true), }, }, models.MTOShipment{ @@ -47,13 +52,15 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { StreetAddress1: "123 Main St", City: "Beverly Hills", State: "CA", - PostalCode: "90210", + PostalCode: beverlyHillsCAPostalCode, + IsOconus: models.BoolPointer(false), }, DestinationAddress: &models.Address{ StreetAddress1: "123 Main St", City: "San Diego", State: "CA", - PostalCode: "92075", + PostalCode: sanDiegoCAPostalCode, + IsOconus: models.BoolPointer(false), }, }, models.MTOShipment{ @@ -63,32 +70,20 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { City: "Wasilla", State: "AK", PostalCode: wasillaAlaskaPostalCode, + IsOconus: models.BoolPointer(true), }, DestinationAddress: &models.Address{ StreetAddress1: "123 Main St", City: "Wasilla", State: "AK", PostalCode: wasillaAlaskaPostalCode, + IsOconus: models.BoolPointer(true), }, }, }, }, } - // create test contract - contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) - suite.NotNil(contract) - suite.FatalNoError(err) - - // setup contract year within availableToPrimeAtTime time - testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ - ReContractYear: models.ReContractYear{ - StartDate: availableToPrimeAtTime, - EndDate: time.Now(), - ContractID: contract.ID, - }, - }) - setupRateArea := func(contract models.ReContract) models.ReRateArea { rateAreaCode := uuid.Must(uuid.NewV4()).String()[0:5] rateArea := models.ReRateArea{ @@ -146,15 +141,46 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { return rateArea } - // setup Fairbanks and Anchorage to have same RateArea - rateArea1 := setupRateAreaToManyPostalCodesData(*contract, []string{fairbanksAlaskaPostalCode, anchorageAlaskaPostalCode}) - // setup Wasilla to have it's own RateArea - rateArea2 := setupRateAreaToPostalCodeData(setupRateArea(*contract), wasillaAlaskaPostalCode) + setupDomesticRateAreaAndZip3s := func(rateAreaCode string, rateAreaName string, postalCodes map[string]string, domesticServiceArea models.ReDomesticServiceArea) (models.ReRateArea, error) { + rateArea := models.ReRateArea{ + ID: uuid.Must(uuid.NewV4()), + ContractID: domesticServiceArea.ContractID, + IsOconus: false, + Code: rateAreaCode, + Name: rateAreaName, + Contract: domesticServiceArea.Contract, + } + verrs, err := suite.DB().ValidateAndCreate(&rateArea) + if verrs.HasAny() { + return rateArea, verrs + } + if err != nil { + return rateArea, err + } - shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) - suite.NotNil(shipmentPostalCodeRateArea) - suite.FatalNoError(err) - suite.Equal(3, len(*shipmentPostalCodeRateArea)) + for postalCode, basePointCity := range postalCodes { + zip3 := models.ReZip3{ + ID: uuid.Must(uuid.NewV4()), + ContractID: domesticServiceArea.ContractID, + Contract: domesticServiceArea.Contract, + Zip3: postalCode[0:3], + RateAreaID: models.UUIDPointer(rateArea.ID), + HasMultipleRateAreas: false, + BasePointCity: basePointCity, + State: "ST", + DomesticServiceAreaID: domesticServiceArea.ID, + } + verrs, err = suite.DB().ValidateAndCreate(&zip3) + if verrs.HasAny() { + return rateArea, verrs + } + if err != nil { + return rateArea, err + } + } + + return rateArea, nil + } isRateAreaEquals := func(expectedRateArea models.ReRateArea, postalCode string, shipmentPostalCodeRateArea *[]services.ShipmentPostalCodeRateArea) bool { var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) @@ -167,16 +193,60 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { return (shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.ID == expectedRateArea.ID && shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.Name == expectedRateArea.Name && shipmentPostalCodeRateAreaLookupMap[postalCode].RateArea.Code == expectedRateArea.Code) } - suite.Equal(true, isRateAreaEquals(rateArea1, fairbanksAlaskaPostalCode, shipmentPostalCodeRateArea)) - suite.Equal(true, isRateAreaEquals(rateArea1, anchorageAlaskaPostalCode, shipmentPostalCodeRateArea)) - suite.Equal(true, isRateAreaEquals(rateArea2, wasillaAlaskaPostalCode, shipmentPostalCodeRateArea)) + // create test contract + contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) + suite.NotNil(contract) + suite.FatalNoError(err) - suite.Equal(false, isRateAreaEquals(rateArea2, fairbanksAlaskaPostalCode, shipmentPostalCodeRateArea)) - suite.Equal(false, isRateAreaEquals(rateArea2, anchorageAlaskaPostalCode, shipmentPostalCodeRateArea)) - suite.Equal(false, isRateAreaEquals(rateArea1, wasillaAlaskaPostalCode, shipmentPostalCodeRateArea)) + // setup contract year within availableToPrimeAtTime time + testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + StartDate: availableToPrimeAtTime, + EndDate: time.Now(), + ContractID: contract.ID, + }, + }) + + domServiceArea := testdatagen.MakeReDomesticServiceArea(suite.DB(), testdatagen.Assertions{ + ReDomesticServiceArea: models.ReDomesticServiceArea{ + ContractID: contract.ID, + }, + }) + + // setup Fairbanks and Anchorage to have same RateArea + rateArea1 := setupRateAreaToManyPostalCodesData(*contract, []string{fairbanksAlaskaPostalCode, anchorageAlaskaPostalCode}) + // setup Wasilla to have it's own RateArea + rateArea2 := setupRateAreaToPostalCodeData(setupRateArea(*contract), wasillaAlaskaPostalCode) + + rateAreaCA, err := setupDomesticRateAreaAndZip3s("US88", "California-South", map[string]string{beverlyHillsCAPostalCode: "Beverly Hills", sanDiegoCAPostalCode: "San Diego"}, domServiceArea) + if err != nil { + suite.Fail(err.Error()) + } + + shipmentPostalCodeRateAreas, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentRateAreas(suite.AppContextForTest(), testMove) + suite.NotNil(shipmentPostalCodeRateAreas) + suite.FatalNoError(err) + suite.Equal(5, len(*shipmentPostalCodeRateAreas)) + + suite.Equal(true, isRateAreaEquals(rateArea1, fairbanksAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(true, isRateAreaEquals(rateArea1, anchorageAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(true, isRateAreaEquals(rateArea2, wasillaAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(true, isRateAreaEquals(rateAreaCA, beverlyHillsCAPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(true, isRateAreaEquals(rateAreaCA, sanDiegoCAPostalCode, shipmentPostalCodeRateAreas)) + + suite.Equal(false, isRateAreaEquals(rateArea2, fairbanksAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea2, anchorageAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea1, wasillaAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateAreaCA, fairbanksAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateAreaCA, anchorageAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateAreaCA, wasillaAlaskaPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea1, beverlyHillsCAPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea1, sanDiegoCAPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea2, beverlyHillsCAPostalCode, shipmentPostalCodeRateAreas)) + suite.Equal(false, isRateAreaEquals(rateArea2, sanDiegoCAPostalCode, shipmentPostalCodeRateAreas)) }) - suite.Run("no oconus rateArea found returns empty array", func() { + suite.Run("Returns matching CONUS rate areas", func() { availableToPrimeAtTime := time.Now().Add(-500 * time.Hour) testMove := models.Move{ AvailableToPrimeAt: &availableToPrimeAtTime, @@ -186,52 +256,107 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { StreetAddress1: "123 Main St", City: "Beverly Hills", State: "CA", - PostalCode: "90210", + PostalCode: beverlyHillsCAPostalCode, + IsOconus: models.BoolPointer(false), }, DestinationAddress: &models.Address{ StreetAddress1: "123 Main St", City: "San Diego", State: "CA", - PostalCode: "92075", + PostalCode: sanDiegoCAPostalCode, + IsOconus: models.BoolPointer(false), }, }, models.MTOShipment{ PPMShipment: &models.PPMShipment{ PickupAddress: &models.Address{ StreetAddress1: "123 Main St", - City: "NY", + City: "Brooklyn", State: "NY", - PostalCode: "11220", + PostalCode: brooklynNYPostalCode, + IsOconus: models.BoolPointer(false), }, DestinationAddress: &models.Address{ StreetAddress1: "123 Main St", City: "Beverly Hills", State: "CA", - PostalCode: "90210", + PostalCode: beverlyHillsCAPostalCode, + IsOconus: models.BoolPointer(false), }, }, }, }, } - // create test contract - contract, err := suite.createContract(suite.AppContextForTest(), testContractCode, testContractName) - suite.NotNil(contract) - suite.FatalNoError(err) + domServiceArea := testdatagen.FetchOrMakeReDomesticServiceArea(suite.DB(), testdatagen.Assertions{ + ReDomesticServiceArea: models.ReDomesticServiceArea{ + ServiceArea: "004", + ServicesSchedule: 2, + }, + ReContract: testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}), + }) + suite.NotNil(domServiceArea) + suite.NotNil(domServiceArea.Contract) // setup contract year within availableToPrimeAtTime time - testdatagen.MakeReContractYear(suite.DB(), testdatagen.Assertions{ + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ ReContractYear: models.ReContractYear{ StartDate: availableToPrimeAtTime, EndDate: time.Now(), - ContractID: contract.ID, + ContractID: domServiceArea.ContractID, }, }) - shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) - suite.NotNil(shipmentPostalCodeRateArea) - suite.Equal(0, len(*shipmentPostalCodeRateArea)) + setupDomesticRateAreaAndZip3s := func(rateAreaCode string, rateAreaName string, postalCodes map[string]string, domesticServiceArea models.ReDomesticServiceArea) (models.ReRateArea, error) { + rateArea := testdatagen.FetchOrMakeReRateArea(suite.DB(), testdatagen.Assertions{ + ReRateArea: models.ReRateArea{ + ContractID: domesticServiceArea.ContractID, + IsOconus: false, + Code: rateAreaCode, + Name: rateAreaName, + Contract: domesticServiceArea.Contract, + }, + }) + + for postalCode, basePointCity := range postalCodes { + testdatagen.FetchOrMakeReZip3(suite.DB(), testdatagen.Assertions{ + ReZip3: models.ReZip3{ + Contract: domesticServiceArea.Contract, + ContractID: domesticServiceArea.ContractID, + DomesticServiceAreaID: domesticServiceArea.ID, + DomesticServiceArea: domesticServiceArea, + Zip3: postalCode[0:3], + BasePointCity: basePointCity, + }, + }) + } + + return rateArea, nil + } + + rateAreaCA, err := setupDomesticRateAreaAndZip3s("US88", "California-South", map[string]string{beverlyHillsCAPostalCode: "Beverly Hills", sanDiegoCAPostalCode: "San Diego"}, domServiceArea) + if err != nil { + suite.Fail(err.Error()) + } + + rateAreaNY, err := setupDomesticRateAreaAndZip3s("US17", "New York", map[string]string{brooklynNYPostalCode: "Brooklyn"}, domServiceArea) + if err != nil { + suite.Fail(err.Error()) + } + + shipmentPostalCodeRateAreas, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentRateAreas(suite.AppContextForTest(), testMove) + suite.NotNil(shipmentPostalCodeRateAreas) + suite.Equal(3, len(*shipmentPostalCodeRateAreas)) suite.Nil(err) + + var shipmentPostalCodeRateAreaLookupMap = make(map[string]services.ShipmentPostalCodeRateArea) + for _, pcra := range *shipmentPostalCodeRateAreas { + shipmentPostalCodeRateAreaLookupMap[pcra.PostalCode] = pcra + } + + suite.Equal(rateAreaCA.Name, shipmentPostalCodeRateAreaLookupMap[beverlyHillsCAPostalCode].RateArea.Name) + suite.Equal(rateAreaCA.Name, shipmentPostalCodeRateAreaLookupMap[sanDiegoCAPostalCode].RateArea.Name) + suite.Equal(rateAreaNY.Name, shipmentPostalCodeRateAreaLookupMap[brooklynNYPostalCode].RateArea.Name) }) suite.Run("not available to prime error", func() { @@ -254,7 +379,7 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { }, } - shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentRateAreas(suite.AppContextForTest(), testMove) suite.Nil(shipmentPostalCodeRateArea) suite.NotNil(err) suite.IsType(apperror.UnprocessableEntityError{}, err) @@ -296,7 +421,7 @@ func (suite *MTOShipmentServiceSuite) TestGetMoveShipmentRateArea() { }, }) - shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentOconusRateArea(suite.AppContextForTest(), testMove) + shipmentPostalCodeRateArea, err := shipmentRateAreaFetcher.GetPrimeMoveShipmentRateAreas(suite.AppContextForTest(), testMove) suite.Nil(shipmentPostalCodeRateArea) suite.NotNil(err) suite.IsType(apperror.NotFoundError{}, err) diff --git a/pkg/services/mto_shipment/mto_shipment_updater.go b/pkg/services/mto_shipment/mto_shipment_updater.go index 1f3696da161..fb75c795a77 100644 --- a/pkg/services/mto_shipment/mto_shipment_updater.go +++ b/pkg/services/mto_shipment/mto_shipment_updater.go @@ -844,7 +844,9 @@ func (f *mtoShipmentUpdater) updateShipmentRecord(appCtx appcontext.AppContext, } // when populating the market_code column, it is considered domestic if both pickup & dest are CONUS addresses - newShipment = models.DetermineShipmentMarketCode(newShipment) + if newShipment.ShipmentType != models.MTOShipmentTypePPM { + newShipment = models.DetermineShipmentMarketCode(newShipment) + } if err := txnAppCtx.DB().Update(newShipment); err != nil { return err diff --git a/pkg/services/mto_shipment/shipment_approver.go b/pkg/services/mto_shipment/shipment_approver.go index ce0cde22c2a..9191657787c 100644 --- a/pkg/services/mto_shipment/shipment_approver.go +++ b/pkg/services/mto_shipment/shipment_approver.go @@ -17,19 +17,23 @@ import ( ) type shipmentApprover struct { - router services.ShipmentRouter - siCreator services.MTOServiceItemCreator - planner route.Planner - moveWeights services.MoveWeights + router services.ShipmentRouter + siCreator services.MTOServiceItemCreator + planner route.Planner + moveWeights services.MoveWeights + moveTaskOrderUpdater services.MoveTaskOrderUpdater + moveRouter services.MoveRouter } // NewShipmentApprover creates a new struct with the service dependencies -func NewShipmentApprover(router services.ShipmentRouter, siCreator services.MTOServiceItemCreator, planner route.Planner, moveWeights services.MoveWeights) services.ShipmentApprover { +func NewShipmentApprover(router services.ShipmentRouter, siCreator services.MTOServiceItemCreator, planner route.Planner, moveWeights services.MoveWeights, moveTaskOrderUpdater services.MoveTaskOrderUpdater, moveRouter services.MoveRouter) services.ShipmentApprover { return &shipmentApprover{ router, siCreator, planner, moveWeights, + moveTaskOrderUpdater, + moveRouter, } } @@ -81,7 +85,7 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { // create international shipment service items before approving // we use a database proc to create the basic auto-approved service items - internationalShipmentTypes := []models.MTOShipmentType{models.MTOShipmentTypeHHG, models.MTOShipmentTypeHHGIntoNTS, models.MTOShipmentTypeUnaccompaniedBaggage} + internationalShipmentTypes := []models.MTOShipmentType{models.MTOShipmentTypeHHG, models.MTOShipmentTypeHHGIntoNTS, models.MTOShipmentTypeHHGOutOfNTS, models.MTOShipmentTypeUnaccompaniedBaggage} if slices.Contains(internationalShipmentTypes, shipment.ShipmentType) && shipment.MarketCode == models.MarketCodeInternational { err := models.CreateApprovedServiceItemsForShipment(appCtx.DB(), shipment) if err != nil { @@ -144,6 +148,13 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen return err } + var move models.Move + move.ID = shipment.MoveTaskOrderID + // re-evaluate move status + if _, err = f.moveRouter.ApproveOrRequestApproval(txnAppCtx, move); err != nil { + return err + } + return nil }) @@ -154,6 +165,29 @@ func (f *shipmentApprover) ApproveShipment(appCtx appcontext.AppContext, shipmen return shipment, nil } +// ApproveShipments Approves one or more shipments in one transaction +func (f *shipmentApprover) ApproveShipments(appCtx appcontext.AppContext, shipments []services.ShipmentIdWithEtag) (*[]models.MTOShipment, error) { + var approvedShipments []models.MTOShipment + + transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { + for _, shipment := range shipments { + shipmentID := shipment.ShipmentID + eTag := shipment.ETag + + approvedShipment, err := f.ApproveShipment(txnAppCtx, shipmentID, eTag) + if err != nil { + return err + } + + approvedShipments = append(approvedShipments, *approvedShipment) + } + + return nil + }) + + return &approvedShipments, transactionError +} + func (f *shipmentApprover) findShipment(appCtx appcontext.AppContext, shipmentID uuid.UUID) (*models.MTOShipment, error) { shipment, err := FindShipment(appCtx, shipmentID, "MoveTaskOrder", "PickupAddress", "DestinationAddress", "StorageFacility") diff --git a/pkg/services/mto_shipment/shipment_approver_test.go b/pkg/services/mto_shipment/shipment_approver_test.go index 4e2a45f68ee..492f0862bad 100644 --- a/pkg/services/mto_shipment/shipment_approver_test.go +++ b/pkg/services/mto_shipment/shipment_approver_test.go @@ -19,8 +19,10 @@ import ( "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/entitlements" "github.com/transcom/mymove/pkg/services/ghcrateengine" + servicesMocks "github.com/transcom/mymove/pkg/services/mocks" shipmentmocks "github.com/transcom/mymove/pkg/services/mocks" moverouter "github.com/transcom/mymove/pkg/services/move" + mt "github.com/transcom/mymove/pkg/services/move_task_order" mtoserviceitem "github.com/transcom/mymove/pkg/services/mto_service_item" "github.com/transcom/mymove/pkg/services/query" "github.com/transcom/mymove/pkg/testdatagen" @@ -36,6 +38,7 @@ type approveShipmentSubtestData struct { mockedShipmentRouter *shipmentmocks.ShipmentRouter reServiceCodes []models.ReServiceCode moveWeights services.MoveWeights + mtoUpdater services.MoveTaskOrderUpdater } // Creates data for the TestApproveShipment function @@ -81,6 +84,31 @@ func (suite *MTOShipmentServiceSuite) createApproveShipmentSubtestData() (subtes factory.FetchReServiceByCode(suite.DB(), serviceCode) } + setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { + mockCreator := &servicesMocks.SignedCertificationCreator{} + + mockCreator.On( + "CreateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + ).Return(returnValue...) + + return mockCreator + } + + setUpSignedCertificationUpdaterMock := func(returnValue ...interface{}) services.SignedCertificationUpdater { + mockUpdater := &servicesMocks.SignedCertificationUpdater{} + + mockUpdater.On( + "UpdateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + mock.AnythingOfType("string"), + ).Return(returnValue...) + + return mockUpdater + } + subtestData.mockedShipmentRouter = &shipmentmocks.ShipmentRouter{} router := NewShipmentRouter() @@ -95,12 +123,19 @@ func (suite *MTOShipmentServiceSuite) createApproveShipmentSubtestData() (subtes mock.Anything, false, ).Return(400, nil) + ppmEstimator := &servicesMocks.PPMEstimator{} + queryBuilder := query.NewQueryBuilder() + subtestData.mtoUpdater = mt.NewMoveTaskOrderUpdater( + queryBuilder, + mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator, + ) siCreator := mtoserviceitem.NewMTOServiceItemCreator(planner, builder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()) subtestData.planner = &mocks.Planner{} subtestData.moveWeights = moverouter.NewMoveWeights(NewShipmentReweighRequester(), waf) - subtestData.shipmentApprover = NewShipmentApprover(router, siCreator, subtestData.planner, subtestData.moveWeights) - subtestData.mockedShipmentApprover = NewShipmentApprover(subtestData.mockedShipmentRouter, siCreator, subtestData.planner, subtestData.moveWeights) + subtestData.shipmentApprover = NewShipmentApprover(router, siCreator, subtestData.planner, subtestData.moveWeights, subtestData.mtoUpdater, moveRouter) + subtestData.mockedShipmentApprover = NewShipmentApprover(subtestData.mockedShipmentRouter, siCreator, subtestData.planner, subtestData.moveWeights, subtestData.mtoUpdater, moveRouter) subtestData.appCtx = suite.AppContextWithSessionForTest(&auth.Session{ ApplicationName: auth.OfficeApp, OfficeUserID: uuid.Must(uuid.NewV4()), @@ -193,6 +228,40 @@ func (suite *MTOShipmentServiceSuite) createApproveShipmentSubtestData() (subtes } func (suite *MTOShipmentServiceSuite) TestApproveShipment() { + setUpSignedCertificationCreatorMock := func(returnValue ...interface{}) services.SignedCertificationCreator { + mockCreator := &servicesMocks.SignedCertificationCreator{} + + mockCreator.On( + "CreateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + ).Return(returnValue...) + + return mockCreator + } + + setUpSignedCertificationUpdaterMock := func(returnValue ...interface{}) services.SignedCertificationUpdater { + mockUpdater := &servicesMocks.SignedCertificationUpdater{} + + mockUpdater.On( + "UpdateSignedCertification", + mock.AnythingOfType("*appcontext.appContext"), + mock.AnythingOfType("models.SignedCertification"), + mock.AnythingOfType("string"), + ).Return(returnValue...) + + return mockUpdater + } + + moveRouter := moverouter.NewMoveRouter() + ppmEstimator := &servicesMocks.PPMEstimator{} + queryBuilder := query.NewQueryBuilder() + planner := &mocks.Planner{} + mtoUpdater := mt.NewMoveTaskOrderUpdater( + queryBuilder, + mtoserviceitem.NewMTOServiceItemCreator(planner, queryBuilder, moveRouter, ghcrateengine.NewDomesticUnpackPricer(), ghcrateengine.NewDomesticPackPricer(), ghcrateengine.NewDomesticLinehaulPricer(), ghcrateengine.NewDomesticShorthaulPricer(), ghcrateengine.NewDomesticOriginPricer(), ghcrateengine.NewDomesticDestinationPricer(), ghcrateengine.NewFuelSurchargePricer()), + moveRouter, setUpSignedCertificationCreatorMock(nil, nil), setUpSignedCertificationUpdaterMock(nil, nil), ppmEstimator, + ) suite.Run("If the international mtoShipment is approved successfully it should create pre approved mtoServiceItems and should NOT update pricing without port data", func() { move := factory.BuildAvailableToPrimeMove(suite.DB(), []factory.Customization{ { @@ -285,7 +354,7 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { ).Return(500, nil) // Approve international shipment - shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) _, err = shipmentApprover.ApproveShipment(appCtx, internationalShipment.ID, internationalShipmentEtag) suite.NoError(err) @@ -371,7 +440,7 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { var moveWeights services.MoveWeights // Approve international shipment - shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) suite.NoError(err) @@ -449,7 +518,7 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { var moveWeights services.MoveWeights // Approve international shipment - shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) suite.NoError(err) @@ -471,6 +540,164 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { } }) + suite.Run("Given international mtoShipment is approved successfully pre-approved mtoServiceItems are created NTS-R CONUS to OCONUS", func() { + storageFacility := factory.BuildStorageFacility(suite.DB(), []factory.Customization{ + { + Model: models.StorageFacility{ + FacilityName: *models.StringPointer("Test Storage Name"), + Email: models.StringPointer("old@email.com"), + LotNumber: models.StringPointer("Test lot number"), + Phone: models.StringPointer("555-555-5555"), + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + }, + }, nil) + + internationalShipment := factory.BuildNTSRShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99507", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: models.MTOShipmentTypeHHGOutOfNTS, + }, + }, + { + Model: storageFacility, + LinkOnly: true, + }, + }, nil) + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + + shipmentRouter := NewShipmentRouter() + var serviceItemCreator services.MTOServiceItemCreator + var planner route.Planner + var moveWeights services.MoveWeights + + // Approve international shipment + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeISLH, + models.ReServiceCodePOEFSC, + models.ReServiceCodeIHUPK, + } + + suite.Equal(len(expectedReserviceCodes), len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + } + }) + + suite.Run("Given international mtoShipment is approved successfully pre-approved mtoServiceItems are created NTS-R OCONUS to CONUS", func() { + storageFacility := factory.BuildStorageFacility(suite.DB(), []factory.Customization{ + { + Model: models.StorageFacility{ + FacilityName: *models.StringPointer("Test Storage Name"), + Email: models.StringPointer("old@email.com"), + LotNumber: models.StringPointer("Test lot number"), + Phone: models.StringPointer("555-555-5555"), + }, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "Anchorage", + State: "AK", + PostalCode: "99507", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + + internationalShipment := factory.BuildNTSRShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + Status: models.MoveStatusAPPROVED, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Des Moines", + State: "IA", + PostalCode: "50314", + IsOconus: models.BoolPointer(false), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + Status: models.MTOShipmentStatusSubmitted, + ShipmentType: models.MTOShipmentTypeHHGOutOfNTS, + }, + }, + { + Model: storageFacility, + LinkOnly: true, + }, + }, nil) + internationalShipmentEtag := etag.GenerateEtag(internationalShipment.UpdatedAt) + + shipmentRouter := NewShipmentRouter() + var serviceItemCreator services.MTOServiceItemCreator + var planner route.Planner + var moveWeights services.MoveWeights + + // Approve international shipment + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) + _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), internationalShipment.ID, internationalShipmentEtag) + suite.NoError(err) + + // Get created pre approved service items + var serviceItems []models.MTOServiceItem + err2 := suite.AppContextForTest().DB().EagerPreload("ReService").Where("mto_shipment_id = ?", internationalShipment.ID).Order("created_at asc").All(&serviceItems) + suite.NoError(err2) + + expectedReserviceCodes := []models.ReServiceCode{ + models.ReServiceCodeISLH, + models.ReServiceCodePODFSC, + models.ReServiceCodeIHUPK, + } + + suite.Equal(len(expectedReserviceCodes), len(serviceItems)) + for i := 0; i < len(serviceItems); i++ { + actualReServiceCode := serviceItems[i].ReService.Code + suite.True(slices.Contains(expectedReserviceCodes, actualReServiceCode)) + } + }) + suite.Run("If the mtoShipment is approved successfully it should create approved mtoServiceItems", func() { subtestData := suite.createApproveShipmentSubtestData() appCtx := subtestData.appCtx @@ -1227,8 +1454,134 @@ func (suite *MTOShipmentServiceSuite) TestApproveShipment() { var moveWeights services.MoveWeights // Approve international shipment - shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights) + shipmentApprover := NewShipmentApprover(shipmentRouter, serviceItemCreator, planner, moveWeights, mtoUpdater, moveRouter) _, err := shipmentApprover.ApproveShipment(suite.AppContextForTest(), invalidShipment.ID, invalidShipmentEtag) suite.Error(err) }) } + +func (suite *MTOShipmentServiceSuite) TestApproveShipments() { + suite.Run("Successfully approves multiple shipments", func() { + subtestData := suite.createApproveShipmentSubtestData() + shipmentApprover := subtestData.shipmentApprover + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + shipment1 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + shipment2 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + eTag1 := etag.GenerateEtag(shipment1.UpdatedAt) + eTag2 := etag.GenerateEtag(shipment2.UpdatedAt) + + shipmentIdWithEtagArr := []services.ShipmentIdWithEtag{ + { + ShipmentID: shipment1.ID, + ETag: eTag1, + }, + { + ShipmentID: shipment2.ID, + ETag: eTag2, + }, + } + approvedShipments, err := shipmentApprover.ApproveShipments(suite.AppContextForTest(), shipmentIdWithEtagArr) + + suite.NoError(err) + suite.NotNil(approvedShipments) + suite.Len(*approvedShipments, 2) + suite.Equal(shipment1.ID, (*approvedShipments)[0].ID) + suite.Equal(shipment2.ID, (*approvedShipments)[1].ID) + }) + + suite.Run("Returns error if one shipment approval fails", func() { + subtestData := suite.createApproveShipmentSubtestData() + shipmentApprover := subtestData.shipmentApprover + + move := factory.BuildAvailableToPrimeMove(suite.DB(), nil, nil) + + shipment1 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + shipment2 := factory.BuildMTOShipment(suite.DB(), []factory.Customization{ + { + Model: move, + LinkOnly: true, + }, + { + Model: models.MTOShipment{ + Status: models.MTOShipmentStatusSubmitted, + }, + }, + }, nil) + + eTag := etag.GenerateEtag(shipment2.UpdatedAt) + + shipmentIdWithEtagArr := []services.ShipmentIdWithEtag{ + { + ShipmentID: shipment1.ID, + ETag: eTag, + }, + { + ShipmentID: shipment2.ID, + ETag: eTag, + }, + } + + approvedShipments, err := shipmentApprover.ApproveShipments(suite.AppContextForTest(), shipmentIdWithEtagArr) + + suite.Error(err) + suite.Len(*approvedShipments, 0) + }) + + suite.Run("Given invalid shipment error returned", func() { + subtestData := suite.createApproveShipmentSubtestData() + shipmentApprover := subtestData.shipmentApprover + invalidShipment := factory.BuildMTOShipment(suite.AppContextForTest().DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + ShipmentType: models.MTOShipmentTypePPM, + }, + }, + }, nil) + invalidShipmentEtag := etag.GenerateEtag(invalidShipment.UpdatedAt) + + shipmentIdWithEtagArr := []services.ShipmentIdWithEtag{ + { + ShipmentID: invalidShipment.ID, + ETag: invalidShipmentEtag, + }, + } + + _, err := shipmentApprover.ApproveShipments(suite.AppContextForTest(), shipmentIdWithEtagArr) + suite.Error(err) + }) +} diff --git a/pkg/services/office_user/office_user_creator.go b/pkg/services/office_user/office_user_creator.go index 87a01051cc4..7f96c3c79ce 100644 --- a/pkg/services/office_user/office_user_creator.go +++ b/pkg/services/office_user/office_user_creator.go @@ -171,8 +171,8 @@ func (o *officeUserCreator) checkAndUpdateRejectedOfficeUser( // checking if the office user currently exists and has a previous status of rejected var requestedOfficeUser models.OfficeUser - previouslyRejectedCheck := query.NewQueryFilter("email", "=", officeUser.Email) - fetchErr := o.builder.FetchOne(appCtx, &requestedOfficeUser, []services.QueryFilter{previouslyRejectedCheck}) + + fetchErr := appCtx.DB().Where("(status = 'REJECTED' and edipi = (?)) or email = (?)", officeUser.EDIPI, officeUser.Email).First(&requestedOfficeUser) if fetchErr != nil && fetchErr != sql.ErrNoRows { return nil, nil, fetchErr // Return the actual error if it's not a "no rows" error } else if fetchErr == sql.ErrNoRows { diff --git a/pkg/services/office_user/office_user_creator_test.go b/pkg/services/office_user/office_user_creator_test.go index 3eee281b96a..0e2075668f9 100644 --- a/pkg/services/office_user/office_user_creator_test.go +++ b/pkg/services/office_user/office_user_creator_test.go @@ -232,6 +232,96 @@ func (suite *OfficeUserServiceSuite) TestCreateOfficeUser() { suite.Nil(updatedOfficeUser.RejectionReason) }) + suite.Run("Updates previously rejected office user instead of create different email same ID", func() { + rejectedStatus := models.OfficeUserStatusREJECTED + requestedStatus := models.OfficeUserStatusREQUESTED + + transportationOffice := factory.BuildTransportationOffice(suite.DB(), []factory.Customization{ + { + Model: models.TransportationOffice{ + ID: uuid.Must(uuid.NewV4()), + }, + }}, nil) + + user := factory.BuildUser(suite.DB(), []factory.Customization{ + { + Model: models.User{ + ID: uuid.Must(uuid.NewV4()), + OktaEmail: "billy+existing@leo.org", + }, + }, + }, nil) + edipi := "1225202401" + officeUser := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + ID: uuid.Must(uuid.NewV4()), + FirstName: "Billy", + LastName: "Bob", + Status: &rejectedStatus, + Email: "billy+was+rejected@leo.org", + EDIPI: &edipi, + }, + }, + { + Model: user, + LinkOnly: true, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + + appCtx := appcontext.NewAppContext(suite.AppContextForTest().DB(), suite.AppContextForTest().Logger(), &auth.Session{}) + queryBuilder := query.NewQueryBuilder() + + officeUserInfo := models.OfficeUser{ + LastName: "Spaceman", + FirstName: "Billy", + Email: "billy+was+sucessful@leo.org", + TransportationOfficeID: transportationOffice.ID, + Telephone: "312-111-1111", + TransportationOffice: transportationOffice, + EDIPI: &edipi, + } + + fakeFetchOne := func(appCtx appcontext.AppContext, model interface{}) error { + switch model.(type) { + case *models.TransportationOffice: + reflect.ValueOf(model).Elem().FieldByName("ID").Set(reflect.ValueOf(transportationOffice.ID)) + case *models.User: + reflect.ValueOf(model).Elem().FieldByName("ID").Set(reflect.ValueOf(officeUser.User.ID)) + reflect.ValueOf(model).Elem().FieldByName("OktaID").Set(reflect.ValueOf(officeUser.User.OktaID)) + reflect.ValueOf(model).Elem().FieldByName("OktaEmail").Set(reflect.ValueOf(officeUser.User.OktaEmail)) + case *models.OfficeUser: + reflect.ValueOf(model).Elem().FieldByName("ID").Set(reflect.ValueOf(officeUser.ID)) + reflect.ValueOf(model).Elem().FieldByName("Email").Set(reflect.ValueOf(officeUser.Email)) + reflect.ValueOf(model).Elem().FieldByName("FirstName").Set(reflect.ValueOf(officeUser.FirstName)) + reflect.ValueOf(model).Elem().FieldByName("LastName").Set(reflect.ValueOf(officeUser.LastName)) + reflect.ValueOf(model).Elem().FieldByName("TransportationOfficeID").Set(reflect.ValueOf(officeUser.TransportationOfficeID)) + reflect.ValueOf(model).Elem().FieldByName("Telephone").Set(reflect.ValueOf(officeUser.Telephone)) + reflect.ValueOf(model).Elem().FieldByName("Status").Set(reflect.ValueOf(officeUser.Status)) + } + return nil + } + + filter := []services.QueryFilter{query.NewQueryFilter("id", "=", transportationOffice.ID)} + + builder := &testOfficeUserQueryBuilder{ + fakeFetchOne: fakeFetchOne, + fakeCreateOne: queryBuilder.CreateOne, + } + mockSender := setUpMockNotificationSender() + + creator := NewOfficeUserCreator(builder, mockSender) + updatedOfficeUser, verrs, err := creator.CreateOfficeUser(appCtx, &officeUserInfo, filter) + suite.NoError(err) + suite.Nil(verrs) + suite.NotNil(updatedOfficeUser) + suite.Equal(updatedOfficeUser.ID, officeUser.ID) + suite.Equal(updatedOfficeUser.Status, &requestedStatus) + suite.Equal(updatedOfficeUser.Email, "billy+was+sucessful@leo.org") + suite.Equal(updatedOfficeUser.EDIPI, &edipi) + suite.Nil(updatedOfficeUser.RejectionReason) + }) + // Bad transportation office ID suite.Run("If we are provided a transportation office that doesn't exist, the create should fail", func() { _, userInfo := setupTestData() diff --git a/pkg/services/office_user/office_user_fetcher.go b/pkg/services/office_user/office_user_fetcher.go index 881c278ac8a..99a6448f1fe 100644 --- a/pkg/services/office_user/office_user_fetcher.go +++ b/pkg/services/office_user/office_user_fetcher.go @@ -82,8 +82,6 @@ func (o *officeUserFetcherPop) FetchOfficeUsersByRoleAndOffice(appCtx appcontext "User", "User.Roles", "User.Privileges", - "TransportationOffice", - "TransportationOffice.Gbloc", ). Join("users", "users.id = office_users.user_id"). Join("users_roles", "users.id = users_roles.user_id"). @@ -109,8 +107,6 @@ func (o *officeUserFetcherPop) FetchSafetyMoveOfficeUsersByRoleAndOffice(appCtx "User", "User.Roles", "User.Privileges", - "TransportationOffice", - "TransportationOffice.Gbloc", ). Join("users", "users.id = office_users.user_id"). Join("users_roles", "users.id = users_roles.user_id"). diff --git a/pkg/services/order/order_updater.go b/pkg/services/order/order_updater.go index e08896380c3..8929d047684 100644 --- a/pkg/services/order/order_updater.go +++ b/pkg/services/order/order_updater.go @@ -485,6 +485,11 @@ func allowanceFromTOOPayload(appCtx appcontext.AppContext, existingOrder models. order.Entitlement.GunSafe = *payload.GunSafe } + if payload.WeightRestriction != nil { + weightRestriction := int(*payload.WeightRestriction) + order.Entitlement.WeightRestriction = &weightRestriction + } + if payload.AccompaniedTour != nil { order.Entitlement.AccompaniedTour = payload.AccompaniedTour } @@ -531,7 +536,6 @@ func allowanceFromTOOPayload(appCtx appcontext.AppContext, existingOrder models. return order, nil } - func allowanceFromCounselingPayload(appCtx appcontext.AppContext, existingOrder models.Order, payload ghcmessages.CounselingUpdateAllowancePayload) (models.Order, error) { order := existingOrder waf := entitlements.NewWeightAllotmentFetcher() @@ -589,6 +593,11 @@ func allowanceFromCounselingPayload(appCtx appcontext.AppContext, existingOrder order.Entitlement.GunSafe = *payload.GunSafe } + if payload.WeightRestriction != nil { + weightRestriction := int(*payload.WeightRestriction) + order.Entitlement.WeightRestriction = &weightRestriction + } + if payload.AccompaniedTour != nil { order.Entitlement.AccompaniedTour = payload.AccompaniedTour } @@ -631,7 +640,6 @@ func allowanceFromCounselingPayload(appCtx appcontext.AppContext, existingOrder return order, nil } - func (f *orderUpdater) saveDocumentForAmendedOrder(appCtx appcontext.AppContext, doc *models.Document) (*models.Document, error) { var docID uuid.UUID if doc != nil { diff --git a/pkg/services/order/order_updater_test.go b/pkg/services/order/order_updater_test.go index 1bf72866552..9e86e990f95 100644 --- a/pkg/services/order/order_updater_test.go +++ b/pkg/services/order/order_updater_test.go @@ -747,6 +747,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { AccompaniedTour: models.BoolPointer(true), DependentsTwelveAndOver: models.Int64Pointer(1), DependentsUnderTwelve: models.Int64Pointer(2), + WeightRestriction: models.Int64Pointer(0), } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), order.ID, payload, eTag) @@ -775,6 +776,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { proGearWeightSpouse := models.Int64Pointer(10) rmeWeight := models.Int64Pointer(10000) eTag := etag.GenerateEtag(order.UpdatedAt) + weightRestriction := models.Int64Pointer(5000) payload := ghcmessages.CounselingUpdateAllowancePayload{ Agency: &affiliation, @@ -784,6 +786,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { ProGearWeight: proGearWeight, ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, + WeightRestriction: weightRestriction, } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), order.ID, payload, eTag) @@ -804,6 +807,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { suite.Equal(*payload.OrganizationalClothingAndIndividualEquipment, updatedOrder.Entitlement.OrganizationalClothingAndIndividualEquipment) suite.EqualValues(payload.Agency, fetchedSM.Affiliation) suite.Equal(*updatedOrder.Entitlement.DBAuthorizedWeight, 16000) + suite.Equal(*payload.WeightRestriction, int64(*updatedOrder.Entitlement.WeightRestriction)) }) suite.Run("Updates the allowance when all fields are valid with dependents present and authorized", func() { @@ -829,6 +833,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { ProGearWeight: proGearWeight, ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, + WeightRestriction: models.Int64Pointer(0), } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), order.ID, payload, eTag) @@ -878,6 +883,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { ProGearWeight: proGearWeight, ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, + WeightRestriction: models.Int64Pointer(0), } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), orderWithoutDefaults.ID, payload, eTag) @@ -929,6 +935,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { ProGearWeight: proGearWeight, ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, + WeightRestriction: models.Int64Pointer(0), } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), order.ID, payload, eTag) @@ -965,6 +972,7 @@ func (suite *OrderServiceSuite) TestUpdateAllowanceAsCounselor() { ProGearWeight: proGearWeight, ProGearWeightSpouse: proGearWeightSpouse, RequiredMedicalEquipmentWeight: rmeWeight, + WeightRestriction: models.Int64Pointer(0), } updatedOrder, _, err := orderUpdater.UpdateAllowanceAsCounselor(suite.AppContextForTest(), order.ID, payload, eTag) diff --git a/pkg/services/payment_request/payment_request_list_fetcher.go b/pkg/services/payment_request/payment_request_list_fetcher.go index 11f0c52805c..fc42b14dc63 100644 --- a/pkg/services/payment_request/payment_request_list_fetcher.go +++ b/pkg/services/payment_request/payment_request_list_fetcher.go @@ -65,6 +65,7 @@ func (f *paymentRequestListFetcher) FetchPaymentRequestList(appCtx appcontext.Ap "MoveTaskOrder.Orders.OriginDutyLocation.TransportationOffice", "MoveTaskOrder.Orders.OriginDutyLocation.Address", "MoveTaskOrder.TIOAssignedUser", + "MoveTaskOrder.TIOAssignedUser.TransportationOfficeID", "MoveTaskOrder.CounselingOffice", // See note further below about having to do this in a separate Load call due to a Pop issue. // "MoveTaskOrder.Orders.ServiceMember", diff --git a/pkg/services/ppm_closeout/ppm_closeout.go b/pkg/services/ppm_closeout/ppm_closeout.go index 925385d079b..7ced6a8c257 100644 --- a/pkg/services/ppm_closeout/ppm_closeout.go +++ b/pkg/services/ppm_closeout/ppm_closeout.go @@ -36,6 +36,9 @@ type serviceItemPrices struct { haulPrice *unit.Cents haulFSC *unit.Cents haulType models.HaulType + intlPackPrice *unit.Cents + intlUnpackPrice *unit.Cents + intlLinehaulPrice *unit.Cents } func NewPPMCloseoutFetcher(planner route.Planner, paymentRequestHelper paymentrequesthelper.Helper, estimator services.PPMEstimator) services.PPMCloseoutFetcher { @@ -60,11 +63,6 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi proGearWeightCustomer, proGearWeightSpouse := p.GetProGearWeights(*ppmShipment) - serviceItems, err := p.getServiceItemPrices(appCtx, *ppmShipment) - if err != nil { - return nil, err - } - var remainingIncentive unit.Cents // Most moves generated by `make db_dev_e2e_populate` skip the part that generates the FinalIncentive for the move, so just return 0. // Moves created through the UI shouldn't be able to skip this part @@ -96,6 +94,11 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi fullWeightGCCShipment.ProGearWeight = &proGearCustomerMax fullWeightGCCShipment.SpouseProGearWeight = &proGearSpouseMax gcc, _ := p.calculateGCC(appCtx, *fullWeightGCCShipment, fullAllowableWeight) + + serviceItems, err := p.getServiceItemPrices(appCtx, *ppmShipment) + if err != nil { + return nil, err + } if serviceItems.storageReimbursementCosts != nil { gcc = gcc.AddCents(*serviceItems.storageReimbursementCosts) } @@ -114,11 +117,14 @@ func (p *ppmCloseoutFetcher) GetPPMCloseout(appCtx appcontext.AppContext, ppmShi ppmCloseoutObj.RemainingIncentive = &remainingIncentive ppmCloseoutObj.HaulPrice = serviceItems.haulPrice ppmCloseoutObj.HaulFSC = serviceItems.haulFSC - ppmCloseoutObj.HaulType = serviceItems.haulType + ppmCloseoutObj.HaulType = &serviceItems.haulType ppmCloseoutObj.DOP = serviceItems.dop ppmCloseoutObj.DDP = serviceItems.ddp ppmCloseoutObj.PackPrice = serviceItems.packPrice ppmCloseoutObj.UnpackPrice = serviceItems.unpackPrice + ppmCloseoutObj.IntlLinehaulPrice = serviceItems.intlLinehaulPrice + ppmCloseoutObj.IntlUnpackPrice = serviceItems.intlUnpackPrice + ppmCloseoutObj.IntlPackPrice = serviceItems.intlPackPrice ppmCloseoutObj.SITReimbursement = serviceItems.storageReimbursementCosts return &ppmCloseoutObj, nil @@ -249,53 +255,6 @@ func (p *ppmCloseoutFetcher) GetExpenseStoragePrice(appCtx appcontext.AppContext return storageExpensePrice, err } -func (p *ppmCloseoutFetcher) GetEntitlement(appCtx appcontext.AppContext, moveID uuid.UUID) (*models.Entitlement, error) { - var moveModel models.Move - err := appCtx.DB().EagerPreload( - "OrdersID", - ).Find(&moveModel, moveID) - - if err != nil { - switch err { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(moveID, "while looking for Move") - default: - return nil, apperror.NewQueryError("Move", err, "unable to find Move") - } - } - - var order models.Order - orderID := &moveModel.OrdersID - errOrder := appCtx.DB().EagerPreload( - "EntitlementID", - ).Find(&order, orderID) - - if errOrder != nil { - switch errOrder { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(*orderID, "while looking for Order") - default: - return nil, apperror.NewQueryError("Order", errOrder, "unable to find Order") - } - } - - var entitlement models.Entitlement - entitlementID := order.EntitlementID - errEntitlement := appCtx.DB().EagerPreload( - "DBAuthorizedWeight", - ).Find(&entitlement, entitlementID) - - if errEntitlement != nil { - switch errEntitlement { - case sql.ErrNoRows: - return nil, apperror.NewNotFoundError(*entitlementID, "while looking for Entitlement") - default: - return nil, apperror.NewQueryError("Entitlement", errEntitlement, "unable to find Entitlement") - } - } - return &entitlement, nil -} - func paramsForServiceCode(code models.ReServiceCode, serviceParams models.ServiceParams) models.ServiceParams { var serviceItemParams models.ServiceParams for _, serviceParam := range serviceParams { @@ -312,17 +271,13 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, var returnPriceObj serviceItemPrices logger := appCtx.Logger() - err := appCtx.DB().Where("mto_shipment_id = ?", ppmShipment.ShipmentID).All(&serviceItemsToPrice) - if err != nil { - return serviceItemPrices{}, err - } + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + serviceItemsToPrice = ppmshipment.BaseServiceItems(ppmShipment) - serviceItemsToPrice = ppmshipment.BaseServiceItems(ppmShipment.ShipmentID) - - // Change DLH to DSH if move within same Zip3 actualPickupPostal := *ppmShipment.ActualPickupPostalCode actualDestPostal := *ppmShipment.ActualDestinationPostalCode - if actualPickupPostal[0:3] == actualDestPostal[0:3] { + // Change DLH to DSH if move within same Zip3 (only for domestic shipments - intl uses ISLH) + if !isInternationalShipment && actualPickupPostal[0:3] == actualDestPostal[0:3] { serviceItemsToPrice[0] = models.MTOServiceItem{ReService: models.ReService{Code: models.ReServiceCodeDSH}, MTOShipmentID: &ppmShipment.ShipmentID} } contractDate := ppmShipment.ExpectedDepartureDate @@ -335,10 +290,12 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, if paramErr != nil { return serviceItemPrices{}, paramErr } - var totalPrice, packPrice, unpackPrice, destinationPrice, originPrice, haulPrice, haulFSC unit.Cents + + var totalPrice, packPrice, unpackPrice, destinationPrice, originPrice, haulPrice, haulFSC, intlPackPrice, intlUnpackPrice, intlLinehaulPrice unit.Cents var totalWeight unit.Pound var ppmToMtoShipment models.MTOShipment + // adding all the weight tickets together to get the total weight of the moved PPM if len(ppmShipment.WeightTickets) >= 1 { for _, weightTicket := range ppmShipment.WeightTickets { if weightTicket.Status != nil && *weightTicket.Status == models.PPMDocumentStatusRejected { @@ -373,14 +330,18 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, return serviceItemPrices{}, err } + // combo of domestic & int'l service items validCodes := map[models.ReServiceCode]string{ - models.ReServiceCodeDPK: "DPK", - models.ReServiceCodeDUPK: "DUPK", - models.ReServiceCodeDOP: "DOP", - models.ReServiceCodeDDP: "DDP", - models.ReServiceCodeDSH: "DSH", - models.ReServiceCodeDLH: "DLH", - models.ReServiceCodeFSC: "FSC", + models.ReServiceCodeDPK: "DPK", + models.ReServiceCodeDUPK: "DUPK", + models.ReServiceCodeDOP: "DOP", + models.ReServiceCodeDDP: "DDP", + models.ReServiceCodeDSH: "DSH", + models.ReServiceCodeDLH: "DLH", + models.ReServiceCodeFSC: "FSC", + models.ReServiceCodeISLH: "ISLH", + models.ReServiceCodeIHPK: "IHPK", + models.ReServiceCodeIHUPK: "IHUPK", } // If service item is of a type we need for a specific calculation, get its price @@ -402,11 +363,11 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, ppmToMtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(p.planner, serviceItemLookups, serviceItem, ppmToMtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(p.planner, serviceItemLookups, serviceItem, ppmToMtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment - err = appCtx.DB().Find(&shipmentWithDistance, ppmShipment.Shipment.ID) + err = appCtx.DB().Eager("PPMShipment").Find(&shipmentWithDistance, ppmShipment.Shipment.ID) if err != nil { logger.Error("could not find shipment in the database") return serviceItemPrices{}, err @@ -419,7 +380,7 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, for _, param := range paramsForServiceCode(serviceItem.ReService.Code, paramsForServiceItems) { paramKey := param.ServiceItemParamKey // This is where the lookup() method of each service item param is actually evaluated - paramValue, serviceParamErr := keyData.ServiceParamValue(appCtx, paramKey.Key) // Fails with "DistanceZip" param? + paramValue, serviceParamErr := keyData.ServiceParamValue(appCtx, paramKey.Key) if serviceParamErr != nil { logger.Error("could not calculate param value lookup", zap.Error(serviceParamErr)) return serviceItemPrices{}, serviceParamErr @@ -452,6 +413,12 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, totalPrice = totalPrice.AddCents(centsValue) switch serviceItem.ReService.Code { + case models.ReServiceCodeIHPK: // Int'l pack + intlPackPrice += centsValue + case models.ReServiceCodeIHUPK: // Int'l unpack + intlUnpackPrice += centsValue + case models.ReServiceCodeISLH: // Int'l shipping & linehaul + intlLinehaulPrice += centsValue case models.ReServiceCodeDPK: packPrice += centsValue case models.ReServiceCodeDUPK: @@ -488,6 +455,9 @@ func (p *ppmCloseoutFetcher) getServiceItemPrices(appCtx appcontext.AppContext, returnPriceObj.storageReimbursementCosts = &sitCosts returnPriceObj.haulPrice = &haulPrice returnPriceObj.haulFSC = &haulFSC + returnPriceObj.intlLinehaulPrice = &intlLinehaulPrice + returnPriceObj.intlPackPrice = &intlPackPrice + returnPriceObj.intlUnpackPrice = &intlUnpackPrice return returnPriceObj, nil } diff --git a/pkg/services/ppm_closeout/ppm_closeout_test.go b/pkg/services/ppm_closeout/ppm_closeout_test.go index ac86dee3f04..cc0fff6a390 100644 --- a/pkg/services/ppm_closeout/ppm_closeout_test.go +++ b/pkg/services/ppm_closeout/ppm_closeout_test.go @@ -24,7 +24,7 @@ const ( ppmBuildWaitingOnCustomer = "waitingOnCustomer" ) -func (suite *PPMCloseoutSuite) TestPPMShipmentCreator() { +func (suite *PPMCloseoutSuite) TestPPMShipmentCloseout() { // One-time test setup mockedPlanner := &mocks.Planner{} diff --git a/pkg/services/ppmshipment/ppm_estimator.go b/pkg/services/ppmshipment/ppm_estimator.go index e49d7846bbe..cf77b16fb04 100644 --- a/pkg/services/ppmshipment/ppm_estimator.go +++ b/pkg/services/ppmshipment/ppm_estimator.go @@ -203,6 +203,12 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip } } + contractDate := newPPMShipment.ExpectedDepartureDate + contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) + if err != nil { + return nil, nil, err + } + calculateSITEstimate := shouldCalculateSITCost(newPPMShipment, &oldPPMShipment) // Clear out any previously calculated SIT estimated costs, if SIT is no longer expected @@ -216,33 +222,66 @@ func (f *estimatePPM) estimateIncentive(appCtx appcontext.AppContext, oldPPMShip return oldPPMShipment.EstimatedIncentive, newPPMShipment.SITEstimatedCost, nil } - contractDate := newPPMShipment.ExpectedDepartureDate - contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) - if err != nil { - return nil, nil, err - } - estimatedIncentive := oldPPMShipment.EstimatedIncentive - if !skipCalculatingEstimatedIncentive { - // Clear out advance and advance requested fields when the estimated incentive is reset. - newPPMShipment.HasRequestedAdvance = nil - newPPMShipment.AdvanceAmountRequested = nil + estimatedSITCost := oldPPMShipment.SITEstimatedCost - estimatedIncentive, err = f.calculatePrice(appCtx, newPPMShipment, 0, contract, false) - if err != nil { - return nil, nil, err + // if the PPM is international, we will use a db func + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { + + if !skipCalculatingEstimatedIncentive { + // Clear out advance and advance requested fields when the estimated incentive is reset. + newPPMShipment.HasRequestedAdvance = nil + newPPMShipment.AdvanceAmountRequested = nil + + estimatedIncentive, err = f.calculatePrice(appCtx, newPPMShipment, 0, contract, false) + if err != nil { + return nil, nil, err + } } - } - estimatedSITCost := oldPPMShipment.SITEstimatedCost - if calculateSITEstimate { - estimatedSITCost, err = CalculateSITCost(appCtx, newPPMShipment, contract) - if err != nil { - return nil, nil, err + if calculateSITEstimate { + estimatedSITCost, err = CalculateSITCost(appCtx, newPPMShipment, contract) + if err != nil { + return nil, nil, err + } + } + + return estimatedIncentive, estimatedSITCost, nil + + } else { + pickupAddress := newPPMShipment.PickupAddress + destinationAddress := newPPMShipment.DestinationAddress + + if !skipCalculatingEstimatedIncentive { + // Clear out advance and advance requested fields when the estimated incentive is reset. + newPPMShipment.HasRequestedAdvance = nil + newPPMShipment.AdvanceAmountRequested = nil + + estimatedIncentive, err = f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newPPMShipment.EstimatedWeight.Int(), true, false, false) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } + } + + if calculateSITEstimate { + var sitAddress models.Address + isOrigin := *newPPMShipment.SITLocation == models.SITLocationTypeOrigin + if isOrigin { + sitAddress = *newPPMShipment.PickupAddress + } else if !isOrigin { + sitAddress = *newPPMShipment.DestinationAddress + } else { + return estimatedIncentive, estimatedSITCost, nil + } + daysInSIT := additionalDaysInSIT(*newPPMShipment.SITEstimatedEntryDate, *newPPMShipment.SITEstimatedDepartureDate) + estimatedSITCost, err = f.CalculateOCONUSSITCosts(appCtx, newPPMShipment.ID, sitAddress.ID, isOrigin, contractDate, newPPMShipment.EstimatedWeight.Int(), daysInSIT) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } } - } - return estimatedIncentive, estimatedSITCost, nil + return estimatedIncentive, estimatedSITCost, nil + } } func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment models.PPMShipment, newPPMShipment *models.PPMShipment, checks ...ppmShipmentValidator) (*unit.Cents, error) { @@ -261,7 +300,7 @@ func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment // we have access to the MoveTaskOrderID in the ppmShipment object so we can use that to get the customer's maximum weight entitlement var move models.Move err = appCtx.DB().Q().Eager( - "Orders.Entitlement", + "Orders.Entitlement", "Orders.OriginDutyLocation.Address", "Orders.NewDutyLocation.Address", ).Where("show = TRUE").Find(&move, newPPMShipment.Shipment.MoveTaskOrderID) if err != nil { return nil, apperror.NewNotFoundError(newPPMShipment.ID, " error querying move") @@ -277,14 +316,27 @@ func (f *estimatePPM) maxIncentive(appCtx appcontext.AppContext, oldPPMShipment return nil, err } - // since the max incentive is based off of the authorized weight entitlement and that value CAN change - // we will calculate the max incentive each time it is called - maxIncentive, err := f.calculatePrice(appCtx, newPPMShipment, unit.Pound(*orders.Entitlement.DBAuthorizedWeight), contract, true) - if err != nil { - return nil, err - } + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { - return maxIncentive, nil + // since the max incentive is based off of the authorized weight entitlement and that value CAN change + // we will calculate the max incentive each time it is called + maxIncentive, err := f.calculatePrice(appCtx, newPPMShipment, unit.Pound(*orders.Entitlement.DBAuthorizedWeight), contract, true) + if err != nil { + return nil, err + } + + return maxIncentive, nil + } else { + pickupAddress := orders.OriginDutyLocation.Address + destinationAddress := orders.NewDutyLocation.Address + + maxIncentive, err := f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, pickupAddress, destinationAddress, contractDate, *orders.Entitlement.DBAuthorizedWeight, false, false, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) + } + + return maxIncentive, nil + } } func (f *estimatePPM) finalIncentive(appCtx appcontext.AppContext, oldPPMShipment models.PPMShipment, newPPMShipment *models.PPMShipment, checks ...ppmShipmentValidator) (*unit.Cents, error) { @@ -307,32 +359,51 @@ func (f *estimatePPM) finalIncentive(appCtx appcontext.AppContext, oldPPMShipmen newTotalWeight = *newPPMShipment.AllowableWeight } - isMissingInfo := shouldSetFinalIncentiveToNil(newPPMShipment, newTotalWeight) - var skipCalculateFinalIncentive bool - finalIncentive := oldPPMShipment.FinalIncentive + contractDate := newPPMShipment.ExpectedDepartureDate + if newPPMShipment.ActualMoveDate != nil { + contractDate = *newPPMShipment.ActualMoveDate + } + contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) + if err != nil { + return nil, err + } + + if newPPMShipment.Shipment.MarketCode != models.MarketCodeInternational { + isMissingInfo := shouldSetFinalIncentiveToNil(newPPMShipment, newTotalWeight) + var skipCalculateFinalIncentive bool + finalIncentive := oldPPMShipment.FinalIncentive + if !isMissingInfo { + skipCalculateFinalIncentive = shouldSkipCalculatingFinalIncentive(newPPMShipment, &oldPPMShipment, originalTotalWeight, newTotalWeight) + if !skipCalculateFinalIncentive { - if !isMissingInfo { - skipCalculateFinalIncentive = shouldSkipCalculatingFinalIncentive(newPPMShipment, &oldPPMShipment, originalTotalWeight, newTotalWeight) - if !skipCalculateFinalIncentive { - contractDate := newPPMShipment.ExpectedDepartureDate - if newPPMShipment.ActualMoveDate != nil { - contractDate = *newPPMShipment.ActualMoveDate - } - contract, err := serviceparamvaluelookups.FetchContract(appCtx, contractDate) - if err != nil { - return nil, err + finalIncentive, err := f.calculatePrice(appCtx, newPPMShipment, newTotalWeight, contract, false) + if err != nil { + return nil, err + } + return finalIncentive, nil } + } else { + finalIncentive = nil - finalIncentive, err = f.calculatePrice(appCtx, newPPMShipment, newTotalWeight, contract, false) + return finalIncentive, nil + } + + return finalIncentive, nil + } else { + pickupAddress := newPPMShipment.PickupAddress + destinationAddress := newPPMShipment.DestinationAddress + + // we can't calculate actual incentive without the weight + if newTotalWeight != 0 { + finalIncentive, err := f.CalculateOCONUSIncentive(appCtx, newPPMShipment.ID, *pickupAddress, *destinationAddress, contractDate, newTotalWeight.Int(), false, true, false) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to calculate estimated PPM incentive: %w", err) } + return finalIncentive, nil + } else { + return nil, nil } - } else { - finalIncentive = nil } - - return finalIncentive, nil } // SumWeightTickets return the total weight of all weightTickets associated with a PPMShipment, returns 0 if there is no valid weight @@ -372,7 +443,7 @@ func (f estimatePPM) calculatePrice(appCtx appcontext.AppContext, ppmShipment *m logger := appCtx.Logger() zeroTotal := false - serviceItemsToPrice := BaseServiceItems(ppmShipment.ShipmentID) + serviceItemsToPrice := BaseServiceItems(*ppmShipment) var move models.Move err := appCtx.DB().Q().Eager( @@ -462,7 +533,7 @@ func (f estimatePPM) calculatePrice(appCtx appcontext.AppContext, ppmShipment *m serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, mtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment @@ -540,7 +611,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m var unpacking unit.Cents var storage unit.Cents - serviceItemsToPrice := BaseServiceItems(ppmShipment.ShipmentID) + serviceItemsToPrice := BaseServiceItems(*ppmShipment) // Replace linehaul pricer with shorthaul pricer if move is within the same Zip3 var pickupPostal, destPostal string @@ -625,7 +696,7 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m serviceItemLookups := serviceparamvaluelookups.InitializeLookups(appCtx, mtoShipment, serviceItem) // This is the struct that gets passed to every param lookup() method that was initialized above - keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code) + keyData := serviceparamvaluelookups.NewServiceItemParamKeyData(f.planner, serviceItemLookups, serviceItem, mtoShipment, contract.Code, contract.ID) // The distance value gets saved to the mto shipment model to reduce repeated api calls. var shipmentWithDistance models.MTOShipment @@ -693,40 +764,117 @@ func (f estimatePPM) priceBreakdown(appCtx appcontext.AppContext, ppmShipment *m return linehaul, fuel, origin, dest, packing, unpacking, storage, nil } -func CalculateSITCost(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, error) { - logger := appCtx.Logger() +// function for calculating incentives for OCONUS PPM shipments +// this uses a db function that takes in values needed to come up with the estimated/actual/max incentives +// this simulates the reimbursement for an iHHG move with ISLH, IHPK, IHUPK, and CONUS portion of FSC +func (f *estimatePPM) CalculateOCONUSIncentive(appCtx appcontext.AppContext, ppmShipmentID uuid.UUID, pickupAddress models.Address, destinationAddress models.Address, moveDate time.Time, weight int, isEstimated bool, isActual bool, isMax bool) (*unit.Cents, error) { + var mileage int + ppmPort, err := models.FetchPortLocationByCode(appCtx.DB(), "4E1") // Tacoma, WA port + if err != nil { + return nil, fmt.Errorf("failed to fetch port location: %w", err) + } + + // check if addresses are OCONUS or CONUS -> this determines how we check mileage to/from the authorized port + isPickupOconus := pickupAddress.IsOconus != nil && *pickupAddress.IsOconus + isDestinationOconus := destinationAddress.IsOconus != nil && *destinationAddress.IsOconus + + switch { + case isPickupOconus && isDestinationOconus: + // OCONUS -> OCONUS, we only reimburse for the CONUS mileage of the PPM + mileage = 0 + case isPickupOconus && !isDestinationOconus: + // OCONUS -> CONUS (port ZIP -> address ZIP) + mileage, err = f.planner.ZipTransitDistance(appCtx, ppmPort.UsPostRegionCity.UsprZipID, destinationAddress.PostalCode, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate OCONUS to CONUS mileage: %w", err) + } + case !isPickupOconus && isDestinationOconus: + // CONUS -> OCONUS (address ZIP -> port ZIP) + mileage, err = f.planner.ZipTransitDistance(appCtx, pickupAddress.PostalCode, ppmPort.UsPostRegionCity.UsprZipID, true) + if err != nil { + return nil, fmt.Errorf("failed to calculate CONUS to OCONUS mileage: %w", err) + } + default: + // covering down on CONUS -> CONUS moves - they should not appear here + return nil, fmt.Errorf("invalid pickup and destination configuration: pickup isOconus=%v, destination isOconus=%v", isPickupOconus, isDestinationOconus) + } + + incentive, err := models.CalculatePPMIncentive(appCtx.DB(), ppmShipmentID, pickupAddress.ID, destinationAddress.ID, moveDate, mileage, weight, isEstimated, isActual, isMax) + if err != nil { + return nil, fmt.Errorf("failed to calculate PPM incentive: %w", err) + } + + return (*unit.Cents)(&incentive.TotalIncentive), nil +} + +func (f *estimatePPM) CalculateOCONUSSITCosts(appCtx appcontext.AppContext, ppmID uuid.UUID, addressID uuid.UUID, isOrigin bool, moveDate time.Time, weight int, sitDays int) (*unit.Cents, error) { + if sitDays <= 0 { + return nil, fmt.Errorf("SIT days must be greater than zero") + } + + if weight <= 0 { + return nil, fmt.Errorf("weight must be greater than zero") + } + sitCosts, err := models.CalculatePPMSITCost(appCtx.DB(), ppmID, addressID, isOrigin, moveDate, weight, sitDays) + if err != nil { + return nil, fmt.Errorf("failed to calculate SIT costs: %w", err) + } + + return (*unit.Cents)(&sitCosts.TotalSITCost), nil +} + +func CalculateSITCost(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, error) { additionalDaysInSIT := additionalDaysInSIT(*ppmShipment.SITEstimatedEntryDate, *ppmShipment.SITEstimatedDepartureDate) - serviceItemsToPrice := StorageServiceItems(ppmShipment.ShipmentID, *ppmShipment.SITLocation, additionalDaysInSIT) + if ppmShipment.Shipment.MarketCode != models.MarketCodeInternational { + logger := appCtx.Logger() - totalPrice := unit.Cents(0) - for _, serviceItem := range serviceItemsToPrice { - pricer, err := ghcrateengine.PricerForServiceItem(serviceItem.ReService.Code) - if err != nil { - logger.Error("unable to find pricer for service item", zap.Error(err)) - return nil, err + serviceItemsToPrice := StorageServiceItems(*ppmShipment, *ppmShipment.SITLocation, additionalDaysInSIT) + + totalPrice := unit.Cents(0) + for _, serviceItem := range serviceItemsToPrice { + pricer, err := ghcrateengine.PricerForServiceItem(serviceItem.ReService.Code) + if err != nil { + logger.Error("unable to find pricer for service item", zap.Error(err)) + return nil, err + } + + var price *unit.Cents + switch serviceItemPricer := pricer.(type) { + case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: + price, _, err = priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) + case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: + price, _, err = priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) + default: + return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) + } + + if err != nil { + return nil, err + } + + logger.Debug(fmt.Sprintf("Price of service item %s %d", serviceItem.ReService.Code, *price)) + totalPrice += *price } - var price *unit.Cents - switch serviceItemPricer := pricer.(type) { - case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: - price, _, err = priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) - case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: - price, _, err = priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) - default: - return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) + return &totalPrice, nil + } else { + var sitAddress models.Address + isOrigin := *ppmShipment.SITLocation == models.SITLocationTypeOrigin + if isOrigin { + sitAddress = *ppmShipment.PickupAddress + } else { + sitAddress = *ppmShipment.DestinationAddress } + contractDate := ppmShipment.ExpectedDepartureDate + totalSITCost, err := models.CalculatePPMSITCost(appCtx.DB(), ppmShipment.ID, sitAddress.ID, isOrigin, contractDate, ppmShipment.SITEstimatedWeight.Int(), additionalDaysInSIT) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to calculate PPM SIT incentive: %w", err) } - - logger.Debug(fmt.Sprintf("Price of service item %s %d", serviceItem.ReService.Code, *price)) - totalPrice += *price + return (*unit.Cents)(&totalSITCost.TotalSITCost), nil } - - return &totalPrice, nil } func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models.PPMShipment, contract models.ReContract) (*models.PPMSITEstimatedCostInfo, error) { @@ -736,7 +884,7 @@ func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models additionalDaysInSIT := additionalDaysInSIT(*ppmShipment.SITEstimatedEntryDate, *ppmShipment.SITEstimatedDepartureDate) - serviceItemsToPrice := StorageServiceItems(ppmShipment.ShipmentID, *ppmShipment.SITLocation, additionalDaysInSIT) + serviceItemsToPrice := StorageServiceItems(*ppmShipment, *ppmShipment.SITLocation, additionalDaysInSIT) totalPrice := unit.Cents(0) for _, serviceItem := range serviceItemsToPrice { @@ -750,8 +898,12 @@ func CalculateSITCostBreakdown(appCtx appcontext.AppContext, ppmShipment *models switch serviceItemPricer := pricer.(type) { case services.DomesticOriginFirstDaySITPricer, services.DomesticDestinationFirstDaySITPricer: price, ppmSITEstimatedCostInfoData, err = calculateFirstDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, ppmSITEstimatedCostInfoData, logger) + case services.IntlOriginFirstDaySITPricer, services.IntlDestinationFirstDaySITPricer: + price, ppmSITEstimatedCostInfoData, err = calculateIntlFirstDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, ppmSITEstimatedCostInfoData, logger) case services.DomesticOriginAdditionalDaysSITPricer, services.DomesticDestinationAdditionalDaysSITPricer: price, ppmSITEstimatedCostInfoData, err = calculateAdditionalDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, additionalDaysInSIT, ppmSITEstimatedCostInfoData, logger) + case services.IntlOriginAdditionalDaySITPricer, services.IntlDestinationAdditionalDaySITPricer: + price, ppmSITEstimatedCostInfoData, err = calculateIntlAdditionalDaySITCostBreakdown(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract, additionalDaysInSIT, ppmSITEstimatedCostInfoData, logger) default: return nil, fmt.Errorf("unknown SIT pricer type found for service item code %s", serviceItem.ReService.Code) } @@ -795,6 +947,33 @@ func calculateFirstDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItem return price, ppmSITEstimatedCostInfoData, nil } +func calculateIntlFirstDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { + price, priceParams, err := priceFirstDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, contract) + if err != nil { + return nil, nil, err + } + ppmSITEstimatedCostInfoData.PriceFirstDaySIT = price + for _, param := range priceParams { + switch param.Key { + case models.ServiceItemParamNameServiceAreaOrigin: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ServiceAreaOrigin = param.Value + case models.ServiceItemParamNameServiceAreaDest: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ServiceAreaDestination = param.Value + case models.ServiceItemParamNameIsPeak: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.IsPeak = param.Value + case models.ServiceItemParamNameContractYearName: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.ContractYearName = param.Value + case models.ServiceItemParamNamePriceRateOrFactor: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.PriceRateOrFactor = param.Value + case models.ServiceItemParamNameEscalationCompounded: + ppmSITEstimatedCostInfoData.ParamsFirstDaySIT.EscalationCompounded = param.Value + default: + logger.Debug(fmt.Sprintf("Unexpected ServiceItemParam in PPM First Day SIT: %s, %s", param.Key, param.Value)) + } + } + return price, ppmSITEstimatedCostInfoData, nil +} + func calculateAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, additionalDaysInSIT int, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { price, priceParams, err := priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) if err != nil { @@ -824,56 +1003,119 @@ func calculateAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, servic return price, ppmSITEstimatedCostInfoData, nil } -func priceFirstDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { - firstDayPricer, ok := pricer.(services.DomesticFirstDaySITPricer) - if !ok { - return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") - } - - // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it - serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode - serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin - if serviceItem.ReService.Code == models.ReServiceCodeDDFSIT { - serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode - serviceAreaKey = models.ServiceItemParamNameServiceAreaDest - } - - serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ - Address: models.Address{PostalCode: serviceAreaPostalCode}, - } - serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) +func calculateIntlAdditionalDaySITCostBreakdown(appCtx appcontext.AppContext, serviceItemPricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract, additionalDaysInSIT int, ppmSITEstimatedCostInfoData *models.PPMSITEstimatedCostInfo, logger *zap.Logger) (*unit.Cents, *models.PPMSITEstimatedCostInfo, error) { + price, priceParams, err := priceAdditionalDaySIT(appCtx, serviceItemPricer, serviceItem, ppmShipment, additionalDaysInSIT, contract) if err != nil { return nil, nil, err } - - serviceAreaParam := services.PricingDisplayParam{ - Key: serviceAreaKey, - Value: serviceArea, + ppmSITEstimatedCostInfoData.PriceAdditionalDaySIT = price + for _, param := range priceParams { + switch param.Key { + case models.ServiceItemParamNameServiceAreaOrigin: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ServiceAreaOrigin = param.Value + case models.ServiceItemParamNameServiceAreaDest: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ServiceAreaDestination = param.Value + case models.ServiceItemParamNameIsPeak: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.IsPeak = param.Value + case models.ServiceItemParamNameContractYearName: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.ContractYearName = param.Value + case models.ServiceItemParamNamePriceRateOrFactor: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.PriceRateOrFactor = param.Value + case models.ServiceItemParamNameEscalationCompounded: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.EscalationCompounded = param.Value + case models.ServiceItemParamNameNumberDaysSIT: + ppmSITEstimatedCostInfoData.ParamsAdditionalDaySIT.NumberDaysSIT = param.Value + default: + logger.Debug(fmt.Sprintf("Unexpected ServiceItemParam in PPM Additional Day SIT: %s, %s", param.Key, param.Value)) + } } + return price, ppmSITEstimatedCostInfoData, nil +} - // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. - if ppmShipment.ActualMoveDate != nil { - price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) +func priceFirstDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { + if serviceItem.ReService.Code == models.ReServiceCodeIOFSIT || serviceItem.ReService.Code == models.ReServiceCodeIDFSIT { + var addressID uuid.UUID + if serviceItem.ReService.Code == models.ReServiceCodeIOFSIT { + addressID = *ppmShipment.PickupAddressID + } else { + addressID = *ppmShipment.DestinationAddressID + } + reServiceID, _ := models.FetchReServiceByCode(appCtx.DB(), serviceItem.ReService.Code) + intlOtherPrice, _ := models.FetchReIntlOtherPrice(appCtx.DB(), addressID, reServiceID.ID, contract.ID, &ppmShipment.ExpectedDepartureDate) + firstDayPricer, ok := pricer.(services.IntlOriginFirstDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) + if err != nil { + return nil, nil, err + } + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } + + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) if err != nil { return nil, nil, err } - pricingParams = append(pricingParams, serviceAreaParam) - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) return &price, pricingParams, nil - } - price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) - if err != nil { - return nil, nil, err - } + } else { + firstDayPricer, ok := pricer.(services.DomesticFirstDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } + + // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it + serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode + serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin + if serviceItem.ReService.Code == models.ReServiceCodeDDFSIT { + serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode + serviceAreaKey = models.ServiceItemParamNameServiceAreaDest + } + + serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ + Address: models.Address{PostalCode: serviceAreaPostalCode}, + } + serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) + if err != nil { + return nil, nil, err + } + + serviceAreaParam := services.PricingDisplayParam{ + Key: serviceAreaKey, + Value: serviceArea, + } + + // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) + if err != nil { + return nil, nil, err + } + + pricingParams = append(pricingParams, serviceAreaParam) + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } + price, pricingParams, err := firstDayPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, true) + if err != nil { + return nil, nil, err + } - pricingParams = append(pricingParams, serviceAreaParam) + pricingParams = append(pricingParams, serviceAreaParam) - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for first day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) - return &price, pricingParams, nil + return &price, pricingParams, nil + } } func additionalDaysInSIT(sitEntryDate time.Time, sitDepartureDate time.Time) int { @@ -887,69 +1129,116 @@ func additionalDaysInSIT(sitEntryDate time.Time, sitDepartureDate time.Time) int } func priceAdditionalDaySIT(appCtx appcontext.AppContext, pricer services.ParamsPricer, serviceItem models.MTOServiceItem, ppmShipment *models.PPMShipment, additionalDaysInSIT int, contract models.ReContract) (*unit.Cents, services.PricingDisplayParams, error) { - additionalDaysPricer, ok := pricer.(services.DomesticAdditionalDaysSITPricer) - if !ok { - return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the additional days pricer interface") - } + // international shipment logic + if serviceItem.ReService.Code == models.ReServiceCodeIOASIT || serviceItem.ReService.Code == models.ReServiceCodeIDASIT { + // address we need for the per_unit_cents is dependent on if it's origin/destination SIT + var addressID uuid.UUID + if serviceItem.ReService.Code == models.ReServiceCodeIOASIT { + addressID = *ppmShipment.PickupAddressID + } else { + addressID = *ppmShipment.DestinationAddressID + } - // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it - serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode - serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin - if serviceItem.ReService.Code == models.ReServiceCodeDDASIT { - serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode - serviceAreaKey = models.ServiceItemParamNameServiceAreaDest - } - serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ - Address: models.Address{PostalCode: serviceAreaPostalCode}, - } + var moveDate time.Time + if ppmShipment.ActualMoveDate != nil { + moveDate = *ppmShipment.ActualMoveDate + } else { + moveDate = ppmShipment.ExpectedDepartureDate + } - serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) - if err != nil { - return nil, nil, err - } + reServiceID, _ := models.FetchReServiceByCode(appCtx.DB(), serviceItem.ReService.Code) + intlOtherPrice, _ := models.FetchReIntlOtherPrice(appCtx.DB(), addressID, reServiceID.ID, contract.ID, &moveDate) - serviceAreaParam := services.PricingDisplayParam{ - Key: serviceAreaKey, - Value: serviceArea, - } + sitDaysParam := services.PricingDisplayParam{ + Key: models.ServiceItemParamNameNumberDaysSIT, + Value: strconv.Itoa(additionalDaysInSIT), + } - sitDaysParam := services.PricingDisplayParam{ - Key: models.ServiceItemParamNameNumberDaysSIT, - Value: strconv.Itoa(additionalDaysInSIT), - } + additionalDayPricer, ok := pricer.(services.IntlOriginAdditionalDaySITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the first day pricer interface") + } - // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. - if ppmShipment.ActualMoveDate != nil { - price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + price, pricingParams, err := additionalDayPricer.Price(appCtx, contract.Code, moveDate, additionalDaysInSIT, *ppmShipment.SITEstimatedWeight, intlOtherPrice.PerUnitCents.Int()) if err != nil { return nil, nil, err } - pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + pricingParams = append(pricingParams, sitDaysParam) appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) return &price, pricingParams, nil - } - price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) - if err != nil { - return nil, nil, err - } + } else { + // domestic PPMs + additionalDaysPricer, ok := pricer.(services.DomesticAdditionalDaysSITPricer) + if !ok { + return nil, nil, errors.New("ppm estimate pricer for SIT service item does not implement the additional days pricer interface") + } - pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + // Need to declare if origin or destination for the serviceAreaLookup, otherwise we already have it + serviceAreaPostalCode := ppmShipment.PickupAddress.PostalCode + serviceAreaKey := models.ServiceItemParamNameServiceAreaOrigin + if serviceItem.ReService.Code == models.ReServiceCodeDDASIT { + serviceAreaPostalCode = ppmShipment.DestinationAddress.PostalCode + serviceAreaKey = models.ServiceItemParamNameServiceAreaDest + } + serviceAreaLookup := serviceparamvaluelookups.ServiceAreaLookup{ + Address: models.Address{PostalCode: serviceAreaPostalCode}, + } - appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + serviceArea, err := serviceAreaLookup.ParamValue(appCtx, contract.Code) + if err != nil { + return nil, nil, err + } + + serviceAreaParam := services.PricingDisplayParam{ + Key: serviceAreaKey, + Value: serviceArea, + } + + sitDaysParam := services.PricingDisplayParam{ + Key: models.ServiceItemParamNameNumberDaysSIT, + Value: strconv.Itoa(additionalDaysInSIT), + } + + // Since this function may be ran before closeout, we need to account for if there's no actual move date yet. + if ppmShipment.ActualMoveDate != nil { + price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, *ppmShipment.ActualMoveDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + if err != nil { + return nil, nil, err + } - return &price, pricingParams, nil + pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } + price, pricingParams, err := additionalDaysPricer.Price(appCtx, contract.Code, ppmShipment.ExpectedDepartureDate, *ppmShipment.SITEstimatedWeight, serviceArea, additionalDaysInSIT, true) + if err != nil { + return nil, nil, err + } + + pricingParams = append(pricingParams, serviceAreaParam, sitDaysParam) + + appCtx.Logger().Debug(fmt.Sprintf("Pricing params for additional day SIT %+v", pricingParams), zap.String("shipmentId", ppmShipment.ShipmentID.String())) + + return &price, pricingParams, nil + } } // mapPPMShipmentEstimatedFields remaps our PPMShipment specific information into the fields where the service param lookups // expect to find them on the MTOShipment model. This is only in-memory and shouldn't get saved to the database. func MapPPMShipmentEstimatedFields(appCtx appcontext.AppContext, ppmShipment models.PPMShipment) (models.MTOShipment, error) { + ppmShipment.Shipment.PPMShipment = &ppmShipment + ppmShipment.Shipment.ShipmentType = models.MTOShipmentTypePPM ppmShipment.Shipment.ActualPickupDate = &ppmShipment.ExpectedDepartureDate ppmShipment.Shipment.RequestedPickupDate = &ppmShipment.ExpectedDepartureDate + ppmShipment.Shipment.PickupAddress = ppmShipment.PickupAddress ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: ppmShipment.PickupAddress.PostalCode} + ppmShipment.Shipment.DestinationAddress = ppmShipment.DestinationAddress ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: ppmShipment.DestinationAddress.PostalCode} ppmShipment.Shipment.PrimeActualWeight = ppmShipment.EstimatedWeight @@ -986,9 +1275,13 @@ func MapPPMShipmentMaxIncentiveFields(appCtx appcontext.AppContext, ppmShipment // expect to find them on the MTOShipment model. This is only in-memory and shouldn't get saved to the database. func MapPPMShipmentFinalFields(ppmShipment models.PPMShipment, totalWeight unit.Pound) models.MTOShipment { + ppmShipment.Shipment.PPMShipment = &ppmShipment + ppmShipment.Shipment.ShipmentType = models.MTOShipmentTypePPM ppmShipment.Shipment.ActualPickupDate = ppmShipment.ActualMoveDate ppmShipment.Shipment.RequestedPickupDate = ppmShipment.ActualMoveDate + ppmShipment.Shipment.PickupAddress = ppmShipment.PickupAddress ppmShipment.Shipment.PickupAddress = &models.Address{PostalCode: *ppmShipment.ActualPickupPostalCode} + ppmShipment.Shipment.DestinationAddress = ppmShipment.DestinationAddress ppmShipment.Shipment.DestinationAddress = &models.Address{PostalCode: *ppmShipment.ActualDestinationPostalCode} ppmShipment.Shipment.PrimeActualWeight = &totalWeight @@ -997,19 +1290,35 @@ func MapPPMShipmentFinalFields(ppmShipment models.PPMShipment, totalWeight unit. // baseServiceItems returns a list of the MTOServiceItems that makeup the price of the estimated incentive. These // are the same non-accesorial service items that get auto-created and approved when the TOO approves an HHG shipment. -func BaseServiceItems(mtoShipmentID uuid.UUID) []models.MTOServiceItem { - return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDLH}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDOP}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDDP}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDPK}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDUPK}, MTOShipmentID: &mtoShipmentID}, +func BaseServiceItems(ppmShipment models.PPMShipment) []models.MTOServiceItem { + mtoShipmentID := ppmShipment.ShipmentID + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + + if isInternationalShipment { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIHPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIHUPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeISLH}, MTOShipmentID: &mtoShipmentID}, + } + } else { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDLH}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeFSC}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDOP}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDDP}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDPK}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDUPK}, MTOShipmentID: &mtoShipmentID}, + } } } -func StorageServiceItems(mtoShipmentID uuid.UUID, locationType models.SITLocationType, additionalDaysInSIT int) []models.MTOServiceItem { - if locationType == models.SITLocationTypeOrigin { +func StorageServiceItems(ppmShipment models.PPMShipment, locationType models.SITLocationType, additionalDaysInSIT int) []models.MTOServiceItem { + mtoShipmentID := ppmShipment.ShipmentID + isInternationalShipment := ppmShipment.Shipment.MarketCode == models.MarketCodeInternational + + // domestic shipments + if locationType == models.SITLocationTypeOrigin && !isInternationalShipment { if additionalDaysInSIT > 0 { return []models.MTOServiceItem{ {ReService: models.ReService{Code: models.ReServiceCodeDOFSIT}, MTOShipmentID: &mtoShipmentID}, @@ -1020,15 +1329,41 @@ func StorageServiceItems(mtoShipmentID uuid.UUID, locationType models.SITLocatio {ReService: models.ReService{Code: models.ReServiceCodeDOFSIT}, MTOShipmentID: &mtoShipmentID}} } - if additionalDaysInSIT > 0 { + if locationType == models.SITLocationTypeDestination && !isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeDDASIT}, MTOShipmentID: &mtoShipmentID}, + } + } + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} + } + + // international shipments + if locationType == models.SITLocationTypeOrigin && isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeIOFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIOASIT}, MTOShipmentID: &mtoShipmentID}, + } + } return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}, - {ReService: models.ReService{Code: models.ReServiceCodeDDASIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIOFSIT}, MTOShipmentID: &mtoShipmentID}} + } + + if locationType == models.SITLocationTypeDestination && isInternationalShipment { + if additionalDaysInSIT > 0 { + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeIDFSIT}, MTOShipmentID: &mtoShipmentID}, + {ReService: models.ReService{Code: models.ReServiceCodeIDASIT}, MTOShipmentID: &mtoShipmentID}, + } } + return []models.MTOServiceItem{ + {ReService: models.ReService{Code: models.ReServiceCodeIDFSIT}, MTOShipmentID: &mtoShipmentID}} } - return []models.MTOServiceItem{ - {ReService: models.ReService{Code: models.ReServiceCodeDDFSIT}, MTOShipmentID: &mtoShipmentID}} + return nil } // paramsForServiceCode filters the list of all service params for service items, to only those matching the service diff --git a/pkg/services/ppmshipment/ppm_estimator_test.go b/pkg/services/ppmshipment/ppm_estimator_test.go index d4ca213d2ac..f384223e3ba 100644 --- a/pkg/services/ppmshipment/ppm_estimator_test.go +++ b/pkg/services/ppmshipment/ppm_estimator_test.go @@ -696,6 +696,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, }, }, nil) + setupPricerData() newPPM := oldPPMShipment newPPM.HasProGear = models.BoolPointer(false) @@ -1559,6 +1560,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) suite.Run("Final Incentive - does not change when required fields are the same", func() { + setupPricerData() oldPPMShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { Model: models.PPMShipment{ @@ -1607,6 +1609,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) suite.Run("Final Incentive - set to nil when missing info", func() { + setupPricerData() oldPPMShipment := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ { Model: models.PPMShipment{ @@ -1698,7 +1701,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }, }, { - Model: &models.Address{ + Model: models.Address{ StreetAddress1: "987 Other Avenue", StreetAddress2: models.StringPointer("P.O. Box 1234"), StreetAddress3: models.StringPointer("c/o Another Person"), @@ -1710,7 +1713,7 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { Type: &factory.Addresses.PickupAddress, }, { - Model: &models.Address{ + Model: models.Address{ StreetAddress1: "987 Other Avenue", StreetAddress2: models.StringPointer("P.O. Box 12345"), StreetAddress3: models.StringPointer("c/o Another Person"), @@ -1760,30 +1763,6 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { SITEstimatedDepartureDate: &entryDate, }, }, - { - Model: models.Address{ - StreetAddress1: "987 Other Avenue", - StreetAddress2: models.StringPointer("P.O. Box 1234"), - StreetAddress3: models.StringPointer("c/o Another Person"), - City: "Des Moines", - State: "IA", - PostalCode: "50309", - County: models.StringPointer("POLK"), - }, - Type: &factory.Addresses.PickupAddress, - }, - { - Model: models.Address{ - StreetAddress1: "987 Other Avenue", - StreetAddress2: models.StringPointer("P.O. Box 12345"), - StreetAddress3: models.StringPointer("c/o Another Person"), - City: "Fort Eisenhower", - State: "GA", - PostalCode: "50309", - County: models.StringPointer("COLUMBIA"), - }, - Type: &factory.Addresses.DeliveryAddress, - }, { Model: mtoShipment, LinkOnly: true, @@ -2049,3 +2028,623 @@ func (suite *PPMShipmentSuite) TestPPMEstimator() { }) }) } + +func (suite *PPMShipmentSuite) TestInternationalPPMEstimator() { + planner := &mocks.Planner{} + paymentRequestHelper := &prhelpermocks.Helper{} + ppmEstimator := NewEstimatePPM(planner, paymentRequestHelper) + + setupPricerData := func() { + contract := testdatagen.FetchOrMakeReContract(suite.DB(), testdatagen.Assertions{}) + startDate := time.Date(2020, time.January, 1, 12, 0, 0, 0, time.UTC) + endDate := time.Date(2020, time.December, 31, 12, 0, 0, 0, time.UTC) + testdatagen.FetchOrMakeReContractYear(suite.DB(), testdatagen.Assertions{ + ReContractYear: models.ReContractYear{ + Contract: contract, + ContractID: contract.ID, + StartDate: startDate, + EndDate: endDate, + Escalation: 1.0, + EscalationCompounded: 1.0, + }, + }) + } + + suite.Run("Estimated Incentive", func() { + suite.Run("Estimated Incentive - Success using estimated weight and not db authorized weight for CONUS -> OCONUS", func() { + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98421", true).Return(3000, nil) + + ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmEstimate) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98421", true) + suite.Equal(unit.Cents(459178), *ppmEstimate) + }) + + suite.Run("Estimated Incentive - Success using estimated weight and not db authorized weight for OCONUS -> CONUS", func() { + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "74133", true).Return(3000, nil) + + ppmEstimate, _, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmEstimate) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "74133", true) + suite.Equal(unit.Cents(423178), *ppmEstimate) + }) + }) + + suite.Run("Max Incentive", func() { + suite.Run("Max Incentive - Success using db authorized weight and not estimated for CONUS -> OCONUS", func() { + oconusAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + destDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + Name: "Test OCONUS Duty Location", + AddressID: oconusAddress.ID, + }, + }, + }, nil) + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + NewDutyLocationID: destDutyLocation.ID, + }, + }, + }, nil) + // when the PPM shipment is in draft, we use the estimated weight and not the db authorized weight + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + OrdersID: order.ID, + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "50309", "98421", true).Return(3000, nil) + + ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmMaxIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "50309", "98421", true) + suite.Equal(unit.Cents(656532), *ppmMaxIncentive) + }) + + suite.Run("Max Incentive - Success using db authorized weight and not estimated for OCONUS -> CONUS", func() { + oconusAddress := factory.BuildAddress(suite.DB(), []factory.Customization{ + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + }, + }, nil) + pickupDutyLocation := factory.BuildDutyLocation(suite.DB(), []factory.Customization{ + { + Model: models.DutyLocation{ + Name: "Test OCONUS Duty Location", + AddressID: oconusAddress.ID, + }, + }, + }, nil) + order := factory.BuildOrder(suite.DB(), []factory.Customization{ + { + Model: models.Order{ + OriginDutyLocationID: &pickupDutyLocation.ID, + }, + }, + }, nil) + // when the PPM shipment is in draft, we use the estimated weight and not the db authorized weight + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.Move{ + OrdersID: order.ID, + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + setupPricerData() + + estimatedWeight := unit.Pound(5000) + newPPM := ppm + newPPM.EstimatedWeight = &estimatedWeight + + // DTOD will be called to get the distance between the origin duty location & the Tacoma Port ZIP + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "30813", true).Return(3000, nil) + + ppmMaxIncentive, err := ppmEstimator.MaxIncentive(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmMaxIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "30813", true) + suite.Equal(unit.Cents(676692), *ppmMaxIncentive) + }) + }) + + suite.Run("Final Incentive", func() { + suite.Run("Final Incentive - Success using estimated weight for CONUS -> OCONUS", func() { + updatedMoveDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + ActualMoveDate: models.TimePointer(updatedMoveDate), + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: models.PoundPointer(4000), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newFullWeight := unit.Pound(8000) + newEmptyWeight := unit.Pound(3000) + newPPM.WeightTickets = models.WeightTickets{ + factory.BuildWeightTicket(suite.DB(), []factory.Customization{ + { + Model: models.WeightTicket{ + FullWeight: &newFullWeight, + EmptyWeight: &newEmptyWeight, + }, + }, + }, nil), + } + + setupPricerData() + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98421", true).Return(3000, nil) + + ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmFinalIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "74133", "98421", true) + suite.Equal(unit.Cents(459178), *ppmFinalIncentive) + }) + + suite.Run("Final Incentive - Success using estimated weight for OCONUS -> CONUS", func() { + updatedMoveDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + ActualMoveDate: models.TimePointer(updatedMoveDate), + Status: models.PPMShipmentStatusWaitingOnCustomer, + EstimatedWeight: models.PoundPointer(4000), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.DeliveryAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.PickupAddress, + }, + }, nil) + + newPPM := ppm + newFullWeight := unit.Pound(8000) + newEmptyWeight := unit.Pound(3000) + newPPM.WeightTickets = models.WeightTickets{ + factory.BuildWeightTicket(suite.DB(), []factory.Customization{ + { + Model: models.WeightTicket{ + FullWeight: &newFullWeight, + EmptyWeight: &newEmptyWeight, + }, + }, + }, nil), + } + + setupPricerData() + + planner.On("ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "74133", true).Return(3000, nil) + + ppmFinalIncentive, err := ppmEstimator.FinalIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(ppmFinalIncentive) + + // it should've called from the pickup -> port and NOT pickup -> dest + planner.AssertCalled(suite.T(), "ZipTransitDistance", mock.AnythingOfType("*appcontext.appContext"), + "98421", "74133", true) + suite.Equal(unit.Cents(423178), *ppmFinalIncentive) + }) + }) + + suite.Run("SIT Costs for OCONUS PPMs", func() { + suite.Run("CalculateSITCost - Success using estimated weight for CONUS -> OCONUS", func() { + originLocation := models.SITLocationTypeOrigin + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(24360), *estimatedSITCost) + }) + + suite.Run("CalculateSITCost - Success using estimated weight for CONUS -> OCONUS", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + _, estimatedSITCost, err := ppmEstimator.EstimateIncentiveWithDefaultChecks(suite.AppContextForTest(), ppm, &newPPM) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(41080), *estimatedSITCost) + }) + + suite.Run("CalculatePPMSITEstimatedCost - Success for OCONUS PPM", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + estimatedSITCost, err := ppmEstimator.CalculatePPMSITEstimatedCost(suite.AppContextForTest(), &ppm) + suite.NilOrNoVerrs(err) + suite.NotNil(estimatedSITCost) + suite.Equal(unit.Cents(20540), *estimatedSITCost) + }) + + suite.Run("CalculatePPMSITEstimatedCostBreakdown - Success for OCONUS PPM", func() { + originLocation := models.SITLocationTypeDestination + entryDate := time.Date(2020, time.March, 15, 0, 0, 0, 0, time.UTC) + ppm := factory.BuildPPMShipment(suite.DB(), []factory.Customization{ + { + Model: models.PPMShipment{ + EstimatedWeight: models.PoundPointer(4000), + SITExpected: models.BoolPointer(true), + SITLocation: &originLocation, + SITEstimatedWeight: models.PoundPointer(unit.Pound(2000)), + SITEstimatedEntryDate: &entryDate, + SITEstimatedDepartureDate: models.TimePointer(entryDate.Add(time.Hour * 24 * 30)), + }, + }, + { + Model: models.MTOShipment{ + MarketCode: models.MarketCodeInternational, + }, + }, + { + Model: models.Address{ + StreetAddress1: "Tester Address", + City: "Tulsa", + State: "OK", + PostalCode: "74133", + }, + Type: &factory.Addresses.PickupAddress, + }, + { + Model: models.Address{ + StreetAddress1: "JBER", + City: "JBER", + State: "AK", + PostalCode: "99505", + IsOconus: models.BoolPointer(true), + }, + Type: &factory.Addresses.DeliveryAddress, + }, + }, nil) + + newPPM := ppm + newEstimatedWeight := models.PoundPointer(5500) + newPPM.SITEstimatedWeight = newEstimatedWeight + setupPricerData() + + sitCosts, err := ppmEstimator.CalculatePPMSITEstimatedCostBreakdown(suite.AppContextForTest(), &ppm) + suite.NilOrNoVerrs(err) + suite.NotNil(sitCosts) + suite.Equal(unit.Cents(20540), *sitCosts.EstimatedSITCost) + suite.Equal(unit.Cents(12140), *sitCosts.PriceFirstDaySIT) + suite.Equal(unit.Cents(8400), *sitCosts.PriceAdditionalDaySIT) + }) + }) +} diff --git a/pkg/services/ppmshipment/ppm_shipment_updater.go b/pkg/services/ppmshipment/ppm_shipment_updater.go index f8cc99b8d30..c52936e9e9f 100644 --- a/pkg/services/ppmshipment/ppm_shipment_updater.go +++ b/pkg/services/ppmshipment/ppm_shipment_updater.go @@ -117,66 +117,6 @@ func (f *ppmShipmentUpdater) updatePPMShipment(appCtx appcontext.AppContext, ppm } transactionError := appCtx.NewTransaction(func(txnAppCtx appcontext.AppContext) error { - // This potentially updates the MTOShipment.Distance field so include it in the transaction - estimatedIncentive, estimatedSITCost, err := f.estimator.EstimateIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - - updatedPPMShipment.EstimatedIncentive = estimatedIncentive - updatedPPMShipment.SITEstimatedCost = estimatedSITCost - - // if the PPM shipment is past closeout then we should not calculate the max incentive, it is already set in stone - if oldPPMShipment.Status != models.PPMShipmentStatusWaitingOnCustomer && - oldPPMShipment.Status != models.PPMShipmentStatusCloseoutComplete && - oldPPMShipment.Status != models.PPMShipmentStatusComplete && - oldPPMShipment.Status != models.PPMShipmentStatusNeedsCloseout { - maxIncentive, err := f.estimator.MaxIncentive(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - updatedPPMShipment.MaxIncentive = maxIncentive - } - - if appCtx.Session() != nil { - if appCtx.Session().IsOfficeUser() { - edited := models.PPMAdvanceStatusEdited - if oldPPMShipment.HasRequestedAdvance != nil && updatedPPMShipment.HasRequestedAdvance != nil { - if !*oldPPMShipment.HasRequestedAdvance && *updatedPPMShipment.HasRequestedAdvance { - updatedPPMShipment.AdvanceStatus = &edited - } else if *oldPPMShipment.HasRequestedAdvance && !*updatedPPMShipment.HasRequestedAdvance { - updatedPPMShipment.AdvanceStatus = &edited - } - } - if oldPPMShipment.AdvanceAmountRequested != nil && updatedPPMShipment.AdvanceAmountRequested != nil { - if *oldPPMShipment.AdvanceAmountRequested != *updatedPPMShipment.AdvanceAmountRequested { - updatedPPMShipment.AdvanceStatus = &edited - } - } - } - if appCtx.Session().IsMilApp() { - if isPrimeCounseled && updatedPPMShipment.HasRequestedAdvance != nil { - received := models.PPMAdvanceStatusReceived - notReceived := models.PPMAdvanceStatusNotReceived - - if updatedPPMShipment.HasReceivedAdvance != nil && *updatedPPMShipment.HasRequestedAdvance { - if *updatedPPMShipment.HasReceivedAdvance { - updatedPPMShipment.AdvanceStatus = &received - } - if !*updatedPPMShipment.HasReceivedAdvance { - updatedPPMShipment.AdvanceStatus = ¬Received - } - } - } - } - } - - finalIncentive, err := f.estimator.FinalIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) - if err != nil { - return err - } - updatedPPMShipment.FinalIncentive = finalIncentive - if updatedPPMShipment.W2Address != nil { var updatedAddress *models.Address var createOrUpdateErr error @@ -282,6 +222,69 @@ func (f *ppmShipmentUpdater) updatePPMShipment(appCtx appcontext.AppContext, ppm updatedPPMShipment.TertiaryDestinationAddress = updatedAddress } + // if the actual move date is being provided, we no longer need to calculate the estimate - it has already happened + if updatedPPMShipment.ActualMoveDate == nil { + estimatedIncentive, estimatedSITCost, err := f.estimator.EstimateIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.EstimatedIncentive = estimatedIncentive + updatedPPMShipment.SITEstimatedCost = estimatedSITCost + } + + // if the PPM shipment is past closeout then we should not calculate the max incentive, it is already set in stone + if oldPPMShipment.Status != models.PPMShipmentStatusWaitingOnCustomer && + oldPPMShipment.Status != models.PPMShipmentStatusCloseoutComplete && + oldPPMShipment.Status != models.PPMShipmentStatusComplete && + oldPPMShipment.Status != models.PPMShipmentStatusNeedsCloseout { + maxIncentive, err := f.estimator.MaxIncentive(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.MaxIncentive = maxIncentive + } + + if appCtx.Session() != nil { + if appCtx.Session().IsOfficeUser() { + edited := models.PPMAdvanceStatusEdited + if oldPPMShipment.HasRequestedAdvance != nil && updatedPPMShipment.HasRequestedAdvance != nil { + if !*oldPPMShipment.HasRequestedAdvance && *updatedPPMShipment.HasRequestedAdvance { + updatedPPMShipment.AdvanceStatus = &edited + } else if *oldPPMShipment.HasRequestedAdvance && !*updatedPPMShipment.HasRequestedAdvance { + updatedPPMShipment.AdvanceStatus = &edited + } + } + if oldPPMShipment.AdvanceAmountRequested != nil && updatedPPMShipment.AdvanceAmountRequested != nil { + if *oldPPMShipment.AdvanceAmountRequested != *updatedPPMShipment.AdvanceAmountRequested { + updatedPPMShipment.AdvanceStatus = &edited + } + } + } + if appCtx.Session().IsMilApp() { + if isPrimeCounseled && updatedPPMShipment.HasRequestedAdvance != nil { + received := models.PPMAdvanceStatusReceived + notReceived := models.PPMAdvanceStatusNotReceived + + if updatedPPMShipment.HasReceivedAdvance != nil && *updatedPPMShipment.HasRequestedAdvance { + if *updatedPPMShipment.HasReceivedAdvance { + updatedPPMShipment.AdvanceStatus = &received + } + if !*updatedPPMShipment.HasReceivedAdvance { + updatedPPMShipment.AdvanceStatus = ¬Received + } + } + } + } + } + + if updatedPPMShipment.ActualMoveDate != nil { + finalIncentive, err := f.estimator.FinalIncentiveWithDefaultChecks(appCtx, *oldPPMShipment, updatedPPMShipment) + if err != nil { + return err + } + updatedPPMShipment.FinalIncentive = finalIncentive + } + verrs, err := appCtx.DB().ValidateAndUpdate(updatedPPMShipment) if verrs != nil && verrs.HasAny() { return apperror.NewInvalidInputError(updatedPPMShipment.ID, err, verrs, "Invalid input found while updating the PPMShipments.") diff --git a/pkg/services/requested_office_users.go b/pkg/services/requested_office_users.go index 574d5915c83..ad917128256 100644 --- a/pkg/services/requested_office_users.go +++ b/pkg/services/requested_office_users.go @@ -1,6 +1,7 @@ package services import ( + "github.com/gobuffalo/pop/v6" "github.com/gobuffalo/validate/v3" "github.com/gofrs/uuid" @@ -13,7 +14,7 @@ import ( // //go:generate mockery --name RequestedOfficeUserListFetcher type RequestedOfficeUserListFetcher interface { - FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []QueryFilter, associations QueryAssociations, pagination Pagination, ordering QueryOrder) (models.OfficeUsers, error) + FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination Pagination, ordering QueryOrder) (models.OfficeUsers, int, error) FetchRequestedOfficeUsersCount(appCtx appcontext.AppContext, filters []QueryFilter) (int, error) } diff --git a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go index b3271f90a9c..9192bf73d6e 100644 --- a/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go +++ b/pkg/services/requested_office_users/requested_office_user_list_fetcher_test.go @@ -1,26 +1,17 @@ package adminuser import ( - "errors" - "reflect" - - "github.com/gofrs/uuid" - "github.com/transcom/mymove/pkg/appcontext" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/models" + "github.com/transcom/mymove/pkg/models/roles" "github.com/transcom/mymove/pkg/services" "github.com/transcom/mymove/pkg/services/pagination" "github.com/transcom/mymove/pkg/services/query" ) type testRequestedOfficeUsersListQueryBuilder struct { - fakeFetchMany func(appCtx appcontext.AppContext, model interface{}) error - fakeCount func(appCtx appcontext.AppContext, model interface{}) (int, error) -} - -func (t *testRequestedOfficeUsersListQueryBuilder) FetchMany(appCtx appcontext.AppContext, model interface{}, _ []services.QueryFilter, _ services.QueryAssociations, _ services.Pagination, _ services.QueryOrder) error { - m := t.fakeFetchMany(appCtx, model) - return m + fakeCount func(appCtx appcontext.AppContext, model interface{}) (int, error) } func (t *testRequestedOfficeUsersListQueryBuilder) Count(appCtx appcontext.AppContext, model interface{}, _ []services.QueryFilter) (int, error) { @@ -33,50 +24,119 @@ func defaultPagination() services.Pagination { return pagination.NewPagination(&page, &perPage) } -func defaultAssociations() services.QueryAssociations { - return query.NewQueryAssociations([]services.QueryAssociation{}) -} - func defaultOrdering() services.QueryOrder { return query.NewQueryOrder(nil, nil) } func (suite *RequestedOfficeUsersServiceSuite) TestFetchRequestedOfficeUserList() { suite.Run("if the users are successfully fetched, they should be returned", func() { - id, err := uuid.NewV4() + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + Status: &requestedStatus, + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + builder := &testRequestedOfficeUsersListQueryBuilder{} + + fetcher := NewRequestedOfficeUsersListFetcher(builder) + + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) + suite.NoError(err) - fakeFetchMany := func(_ appcontext.AppContext, model interface{}) error { - value := reflect.ValueOf(model).Elem() - requestedStatus := models.OfficeUserStatusREQUESTED - value.Set(reflect.Append(value, reflect.ValueOf(models.OfficeUser{ID: id, Status: &requestedStatus}))) - return nil - } - builder := &testRequestedOfficeUsersListQueryBuilder{ - fakeFetchMany: fakeFetchMany, - } + suite.Equal(officeUser1.ID, requestedOfficeUsers[0].ID) + }) + + suite.Run("if there are no requested office users, we don't receive any requested office users", func() { + builder := &testRequestedOfficeUsersListQueryBuilder{} fetcher := NewRequestedOfficeUsersListFetcher(builder) - requestedOfficeUsers, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultAssociations(), defaultPagination(), defaultOrdering()) + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), defaultOrdering()) suite.NoError(err) - suite.Equal(id, requestedOfficeUsers[0].ID) + suite.Equal(models.OfficeUsers(nil), requestedOfficeUsers) }) - suite.Run("if there is an error, we get it with no requested office users", func() { - fakeFetchMany := func(_ appcontext.AppContext, _ interface{}) error { - return errors.New("Fetch error") - } - builder := &testRequestedOfficeUsersListQueryBuilder{ - fakeFetchMany: fakeFetchMany, - } + suite.Run("should sort and order requested office users", func() { + requestedStatus := models.OfficeUserStatusREQUESTED + officeUser1 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Angelina", + LastName: "Jolie", + Email: "laraCroft@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Kirtland AFB - USAF", + }, + }, + }, []roles.RoleType{roles.RoleTypeTOO}) + officeUser2 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Billy", + LastName: "Bob", + Email: "bigBob@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Fort Knox - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeTIO}) + officeUser3 := factory.BuildOfficeUserWithRoles(suite.DB(), []factory.Customization{ + { + Model: models.OfficeUser{ + FirstName: "Nick", + LastName: "Cage", + Email: "conAirKilluh@mail.mil", + Status: &requestedStatus, + }, + }, + { + Model: models.TransportationOffice{ + Name: "PPPO Detroit Arsenal - USA", + }, + }, + }, []roles.RoleType{roles.RoleTypeServicesCounselor}) + + builder := &testRequestedOfficeUsersListQueryBuilder{} fetcher := NewRequestedOfficeUsersListFetcher(builder) - requestedOfficeUsers, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), []services.QueryFilter{}, defaultAssociations(), defaultPagination(), defaultOrdering()) + column := "transportation_office_id" + ordering := query.NewQueryOrder(&column, models.BoolPointer(true)) + + requestedOfficeUsers, _, err := fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(requestedOfficeUsers, 3) + suite.Equal(officeUser3.ID.String(), requestedOfficeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), requestedOfficeUsers[1].ID.String()) + suite.Equal(officeUser1.ID.String(), requestedOfficeUsers[2].ID.String()) + + ordering = query.NewQueryOrder(&column, models.BoolPointer(false)) + + requestedOfficeUsers, _, err = fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) + + suite.NoError(err) + suite.Len(requestedOfficeUsers, 3) + suite.Equal(officeUser1.ID.String(), requestedOfficeUsers[0].ID.String()) + suite.Equal(officeUser2.ID.String(), requestedOfficeUsers[1].ID.String()) + suite.Equal(officeUser3.ID.String(), requestedOfficeUsers[2].ID.String()) + + column = "unknown_column" + + requestedOfficeUsers, _, err = fetcher.FetchRequestedOfficeUsersList(suite.AppContextForTest(), nil, defaultPagination(), ordering) suite.Error(err) - suite.Equal(err.Error(), "Fetch error") - suite.Equal(models.OfficeUsers(nil), requestedOfficeUsers) + suite.Len(requestedOfficeUsers, 0) }) } diff --git a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go index 29004b8485e..3c883225b3b 100644 --- a/pkg/services/requested_office_users/requested_office_users_list_fetcher.go +++ b/pkg/services/requested_office_users/requested_office_users_list_fetcher.go @@ -1,13 +1,17 @@ package adminuser import ( + "fmt" + "sort" + + "github.com/gobuffalo/pop/v6" + "github.com/transcom/mymove/pkg/appcontext" "github.com/transcom/mymove/pkg/models" "github.com/transcom/mymove/pkg/services" ) type requestedOfficeUsersListQueryBuilder interface { - FetchMany(appCtx appcontext.AppContext, model interface{}, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) error Count(appCtx appcontext.AppContext, model interface{}, filters []services.QueryFilter) (int, error) } @@ -16,10 +20,57 @@ type requestedOfficeUserListFetcher struct { } // FetchAdminUserList uses the passed query builder to fetch a list of office users -func (o *requestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filters []services.QueryFilter, associations services.QueryAssociations, pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, error) { +func (o *requestedOfficeUserListFetcher) FetchRequestedOfficeUsersList(appCtx appcontext.AppContext, filterFuncs []func(*pop.Query), pagination services.Pagination, ordering services.QueryOrder) (models.OfficeUsers, int, error) { + var query *pop.Query var requestedUsers models.OfficeUsers - err := o.builder.FetchMany(appCtx, &requestedUsers, filters, associations, pagination, ordering) - return requestedUsers, err + + query = appCtx.DB().Q().EagerPreload( + "User.Roles", + "TransportationOffice"). + Join("users", "users.id = office_users.user_id"). + Join("users_roles", "users.id = users_roles.user_id"). + Join("roles", "users_roles.role_id = roles.id"). + Join("transportation_offices", "office_users.transportation_office_id = transportation_offices.id") + + for _, filterFunc := range filterFuncs { + filterFunc(query) + } + + query = query.Where("status = ?", models.OfficeUserStatusREQUESTED) + query.GroupBy("office_users.id") + + var order = "desc" + if ordering.SortOrder() != nil && *ordering.SortOrder() { + order = "asc" + } + + var orderTerm = "id" + if ordering.Column() != nil { + orderTerm = *ordering.Column() + } + + query.Order(fmt.Sprintf("%s %s", orderTerm, order)) + query.Select("office_users.*") + + err := query.Paginate(pagination.Page(), pagination.PerPage()).All(&requestedUsers) + if err != nil { + return nil, 0, err + } + + if orderTerm == "transportation_office_id" { + if order == "desc" { + sort.Slice(requestedUsers, func(i, j int) bool { + return requestedUsers[i].TransportationOffice.Name > requestedUsers[j].TransportationOffice.Name + }) + } else { + sort.Slice(requestedUsers, func(i, j int) bool { + return requestedUsers[i].TransportationOffice.Name < requestedUsers[j].TransportationOffice.Name + }) + } + } + + count := query.Paginator.TotalEntriesSize + return requestedUsers, count, nil } // FetchAdminUserList uses the passed query builder to fetch a list of office users diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go index a3ecc01ee96..90b187f0be0 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet.go @@ -93,7 +93,11 @@ func (SSWPPMComputer *SSWPPMComputer) FormatValuesShipmentSummaryWorksheet(shipm A. the difference between the GTCC paid expenses and the GCC OR B. the total member-paid expenses plus the member paid SIT **/ - if shipmentSummaryFormData.IsActualExpenseReimbursement { + if shipmentSummaryFormData.IsActualExpenseReimbursement && isPaymentPacket { + if shipmentSummaryFormData.PPMShipment.FinalIncentive == nil { + return services.Page1Values{}, services.Page2Values{}, services.Page3Values{}, fmt.Errorf("missing FinalIncentive: required for actual expense reimbursement (Order ID: %s)", shipmentSummaryFormData.Order.ID) + } + floatFinalIncentive := shipmentSummaryFormData.PPMShipment.FinalIncentive.ToDollarFloatNoRound() // FinalIncentive == ActualGCC if sumGTCC < floatFinalIncentive { // There are funds left over after GTCC to pay out member expenses diff --git a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go index ae83dae1b9f..7eca297f3a6 100644 --- a/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go +++ b/pkg/services/shipment_summary_worksheet/shipment_summary_worksheet_test.go @@ -1452,7 +1452,7 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestActualExpenseReimbursemen // Also test for AOA instead of payment packet page1Data, page2Data, Page3Data, err = sswPPMComputer.FormatValuesShipmentSummaryWorksheet(ssd, false) suite.NoError(err) - suite.Equal(expectedDisbursementString(20000, 0), page2Data.Disbursement) + suite.Equal("N/A", page2Data.Disbursement) suite.Equal("$0.00", page2Data.PPMRemainingEntitlement) // Check PDF generation again @@ -1485,7 +1485,7 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestActualExpenseReimbursemen page1Data, page2Data, Page3Data, err = sswPPMComputer.FormatValuesShipmentSummaryWorksheet(ssd, false) suite.NoError(err) - suite.Equal(expectedDisbursementString(11500, 8500), page2Data.Disbursement) + suite.Equal("N/A", page2Data.Disbursement) suite.Equal("$0.00", page2Data.PPMRemainingEntitlement) test, info, err = ppmGenerator.FillSSWPDFForm(page1Data, page2Data, Page3Data) @@ -1517,7 +1517,7 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestActualExpenseReimbursemen page1Data, page2Data, Page3Data, err = sswPPMComputer.FormatValuesShipmentSummaryWorksheet(ssd, false) suite.NoError(err) - suite.Equal(expectedDisbursementString(11500, 3000), page2Data.Disbursement) + suite.Equal("N/A", page2Data.Disbursement) suite.Equal("$0.00", page2Data.PPMRemainingEntitlement) test, info, err = ppmGenerator.FillSSWPDFForm(page1Data, page2Data, Page3Data) @@ -1992,3 +1992,70 @@ func (suite *ShipmentSummaryWorksheetServiceSuite) TestFormatDisbursement() { result = formatDisbursement(expensesMap, ppmRemainingEntitlement) suite.Equal(expectedResult, result) } + +func (suite *ShipmentSummaryWorksheetServiceSuite) TestAOAPaymentPacketWithNilFinalIncentive() { + mockPPMCloseoutFetcher := &mocks.PPMCloseoutFetcher{} + sswPPMComputer := NewSSWPPMComputer(mockPPMCloseoutFetcher) + fortGordon := factory.FetchOrBuildOrdersDutyLocation(suite.DB()) + orderIssueDate := time.Date(2018, time.December, 21, 0, 0, 0, 0, time.UTC) + locator := "ABCDEF-01" + + shipment := models.PPMShipment{ + Shipment: models.MTOShipment{ + ShipmentLocator: &locator, + }, + IsActualExpenseReimbursement: models.BoolPointer(true), + } + + order := models.Order{ + IssueDate: orderIssueDate, + OrdersType: internalmessages.OrdersTypePERMANENTCHANGEOFSTATION, + OrdersNumber: models.StringPointer("012345"), + NewDutyLocationID: fortGordon.ID, + TAC: models.StringPointer("NTA4"), + SAC: models.StringPointer("SAC"), + HasDependents: true, + SpouseHasProGear: true, + } + storageExpense := models.MovingExpenseReceiptTypeStorage + contractedExpense := models.MovingExpenseReceiptTypeContractedExpense + movingExpenses := models.MovingExpenses{ + { + MovingExpenseType: &contractedExpense, + PaidWithGTCC: models.BoolPointer(false), + }, + { + MovingExpenseType: &contractedExpense, + PaidWithGTCC: models.BoolPointer(true), + }, + { + MovingExpenseType: &storageExpense, + PaidWithGTCC: models.BoolPointer(false), + }, + { + MovingExpenseType: &storageExpense, + PaidWithGTCC: models.BoolPointer(true), + }, + } + signedCertType := models.SignedCertificationTypeCloseoutReviewedPPMPAYMENT + cert := models.SignedCertification{ + CertificationType: &signedCertType, + CertificationText: "APPROVED", + Signature: "Firstname Lastname", + UpdatedAt: time.Now(), + PpmID: models.UUIDPointer(shipment.ID), + } + var certs []*models.SignedCertification + certs = append(certs, &cert) + + ssd := models.ShipmentSummaryFormData{ + Order: order, + MovingExpenses: movingExpenses, + PPMShipment: shipment, + SignedCertifications: certs, + IsActualExpenseReimbursement: true, + } + _, _, _, err := sswPPMComputer.FormatValuesShipmentSummaryWorksheet(ssd, true) + suite.Error(err) + suite.Contains(err.Error(), "missing FinalIncentive: required for actual expense reimbursement") +} diff --git a/pkg/services/transportation_office.go b/pkg/services/transportation_office.go index 307f0d0fef5..7255991f376 100644 --- a/pkg/services/transportation_office.go +++ b/pkg/services/transportation_office.go @@ -9,8 +9,8 @@ import ( //go:generate mockery --name TransportationOfficesFetcher type TransportationOfficesFetcher interface { - GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) + GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) GetTransportationOffice(appCtx appcontext.AppContext, transportationOfficeID uuid.UUID, includeOnlyPPMCloseoutOffices bool) (*models.TransportationOffice, error) GetAllGBLOCs(appCtx appcontext.AppContext) (*models.GBLOCs, error) - GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID) (*models.TransportationOffices, error) + GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID, serviceMemberID uuid.UUID) (*models.TransportationOffices, error) } diff --git a/pkg/services/transportation_office/transportation_office_fetcher.go b/pkg/services/transportation_office/transportation_office_fetcher.go index be942890c75..8fde2e98093 100644 --- a/pkg/services/transportation_office/transportation_office_fetcher.go +++ b/pkg/services/transportation_office/transportation_office_fetcher.go @@ -46,8 +46,8 @@ func (o transportationOfficesFetcher) GetTransportationOffice(appCtx appcontext. return &transportationOffice, nil } -func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool) (*models.TransportationOffices, error) { - officeList, err := FindTransportationOffice(appCtx, search, forPpm) +func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (*models.TransportationOffices, error) { + officeList, err := FindTransportationOffice(appCtx, search, forPpm, forAdminOfficeUserReqFilter) if err != nil { switch err { @@ -61,9 +61,15 @@ func (o transportationOfficesFetcher) GetTransportationOffices(appCtx appcontext return &officeList, nil } -func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPpm bool) (models.TransportationOffices, error) { +func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPpm bool, forAdminOfficeUserReqFilter bool) (models.TransportationOffices, error) { var officeList []models.TransportationOffice + // Changing return limit for Admin Requested Office Users Transportation Office Filter implementation + var limit = 5 + if forAdminOfficeUserReqFilter { + limit = 50 + } + // The % operator filters out strings that are below this similarity threshold err := appCtx.DB().Q().RawQuery("SET pg_trgm.similarity_threshold = 0.03").Exec() if err != nil { @@ -80,13 +86,13 @@ func FindTransportationOffice(appCtx appcontext.AppContext, search string, forPp } sqlQuery += ` order by sim desc - limit 5) + limit $2) select office.* from names n inner join transportation_offices office on n.transportation_office_id = office.id group by office.id order by max(n.sim) desc, office.name - limit 5` - query := appCtx.DB().Q().RawQuery(sqlQuery, search) + limit $2` + query := appCtx.DB().Q().RawQuery(sqlQuery, search, limit) if err := query.All(&officeList); err != nil { if errors.Cause(err).Error() != models.RecordNotFoundErrorString { return officeList, err @@ -127,8 +133,8 @@ func ListDistinctGBLOCs(appCtx appcontext.AppContext) (models.GBLOCs, error) { return gblocList, err } -func (o transportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID) (*models.TransportationOffices, error) { - officeList, err := findCounselingOffice(appCtx, dutyLocationID) +func (o transportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.AppContext, dutyLocationID uuid.UUID, serviceMemberID uuid.UUID) (*models.TransportationOffices, error) { + officeList, err := findCounselingOffice(appCtx, dutyLocationID, serviceMemberID) if err != nil { switch err { @@ -143,7 +149,8 @@ func (o transportationOfficesFetcher) GetCounselingOffices(appCtx appcontext.App } // return all the transportation offices in the GBLOC of the given duty location where provides_services_counseling = true -func findCounselingOffice(appCtx appcontext.AppContext, dutyLocationID uuid.UUID) (models.TransportationOffices, error) { +// serviceMemberID is only provided when this function is called by the office handler +func findCounselingOffice(appCtx appcontext.AppContext, dutyLocationID uuid.UUID, serviceMemberID uuid.UUID) (models.TransportationOffices, error) { var officeList []models.TransportationOffice duty_location, err := models.FetchDutyLocation(appCtx.DB(), dutyLocationID) @@ -157,7 +164,7 @@ func findCounselingOffice(appCtx appcontext.AppContext, dutyLocationID uuid.UUID // Find for oconus duty location // ******************************** if *duty_location.Address.IsOconus { - gblocDepartmentIndicator, err := findOconusGblocDepartmentIndicator(appCtx, duty_location) + gblocDepartmentIndicator, err := findOconusGblocDepartmentIndicator(appCtx, duty_location, serviceMemberID) if err != nil { return officeList, err } @@ -238,8 +245,8 @@ func findCounselingOffice(appCtx appcontext.AppContext, dutyLocationID uuid.UUID return officeList, nil } -func findOconusGblocDepartmentIndicator(appCtx appcontext.AppContext, dutyLocation models.DutyLocation) (*oconusGblocDepartmentIndicator, error) { - serviceMember, err := models.FetchServiceMember(appCtx.DB(), appCtx.Session().ServiceMemberID) +func findOconusGblocDepartmentIndicator(appCtx appcontext.AppContext, dutyLocation models.DutyLocation, serviceMemberID uuid.UUID) (*oconusGblocDepartmentIndicator, error) { + serviceMember, err := models.FetchServiceMember(appCtx.DB(), serviceMemberID) if err != nil { return nil, err } diff --git a/pkg/services/transportation_office/transportation_office_fetcher_test.go b/pkg/services/transportation_office/transportation_office_fetcher_test.go index cd8345a8aa6..64c377deb30 100644 --- a/pkg/services/transportation_office/transportation_office_fetcher_test.go +++ b/pkg/services/transportation_office/transportation_office_fetcher_test.go @@ -42,7 +42,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SearchTransportationOffice() }, }, }, nil) - office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true, false) suite.NoError(err) suite.Equal(transportationOffice.Name, office[0].Name) @@ -53,7 +53,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SearchTransportationOffice() func (suite *TransportationOfficeServiceSuite) Test_SearchWithNoTransportationOffices() { - office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "LRC Fort Knox", true, false) suite.NoError(err) suite.Len(office, 0) } @@ -87,7 +87,7 @@ func (suite *TransportationOfficeServiceSuite) Test_SortedTransportationOffices( }, }, nil) - office, err := FindTransportationOffice(suite.AppContextForTest(), "JPPSO", true) + office, err := FindTransportationOffice(suite.AppContextForTest(), "JPPSO", true, false) suite.NoError(err) suite.Equal(transportationOffice1.Name, office[0].Name) @@ -180,7 +180,16 @@ func (suite *TransportationOfficeServiceSuite) Test_FindCounselingOffices() { }, }, nil) - offices, err := findCounselingOffice(suite.AppContextForTest(), origDutyLocation.ID) + armyAffliation := models.AffiliationARMY + serviceMember := factory.BuildServiceMember(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &armyAffliation, + }, + }, + }, nil) + + offices, err := findCounselingOffice(suite.AppContextForTest(), origDutyLocation.ID, serviceMember.ID) suite.NoError(err) suite.Len(offices, 2) @@ -294,7 +303,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi appCtx := suite.AppContextWithSessionForTest(&auth.Session{ ServiceMemberID: serviceMember.ID, }) - departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation) + departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation, serviceMember.ID) suite.NotNil(departmentIndictor) suite.Nil(err) suite.Nil(departmentIndictor.DepartmentIndicator) @@ -320,7 +329,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi ServiceMemberID: serviceMember.ID, }) suite.Nil(err) - departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation) + departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation, appCtx.Session().ServiceMemberID) suite.NotNil(departmentIndictor) suite.Nil(err) suite.NotNil(departmentIndictor.DepartmentIndicator) @@ -336,7 +345,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi ServiceMemberID: uuid.Must(uuid.NewV4()), }) - departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation) + departmentIndictor, err := findOconusGblocDepartmentIndicator(appCtx, dutylocation, appCtx.Session().ServiceMemberID) suite.Nil(departmentIndictor) suite.NotNil(err) }) @@ -346,7 +355,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi ServiceMemberID: uuid.Must(uuid.NewV4()), }) unknown_duty_location_id := uuid.Must(uuid.NewV4()) - offices, err := findCounselingOffice(appCtx, unknown_duty_location_id) + offices, err := findCounselingOffice(appCtx, unknown_duty_location_id, appCtx.Session().ServiceMemberID) suite.Nil(offices) suite.NotNil(err) }) @@ -369,7 +378,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi }) suite.Nil(err) - offices, err := findCounselingOffice(appCtx, dutylocation.ID) + offices, err := findCounselingOffice(appCtx, dutylocation.ID, appCtx.Session().ServiceMemberID) suite.NotNil(offices) suite.Nil(err) suite.Equal(1, len(offices)) @@ -385,7 +394,7 @@ func (suite *TransportationOfficeServiceSuite) Test_Oconus_AK_FindCounselingOffi }, }, }, nil) - offices, err = findCounselingOffice(appCtx, dutylocation.ID) + offices, err = findCounselingOffice(appCtx, dutylocation.ID, appCtx.Session().ServiceMemberID) suite.NotNil(offices) suite.Nil(err) suite.Equal(2, len(offices)) diff --git a/pkg/testdatagen/scenario/shared.go b/pkg/testdatagen/scenario/shared.go index e380cd47da1..3eec4a8f5ee 100644 --- a/pkg/testdatagen/scenario/shared.go +++ b/pkg/testdatagen/scenario/shared.go @@ -4222,11 +4222,15 @@ func createHHGWithOriginSITServiceItems( handlerConfig := handlers.Config{} ppmEstimator := ppmshipment.NewEstimatePPM(handlerConfig.DTODPlanner(), &paymentrequesthelper.RequestPaymentHelper{}) mtoUpdater := movetaskorder.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, signedCertificationCreator, signedCertificationUpdater, ppmEstimator) - _, approveErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) + _, approveErr := mtoUpdater.ApproveMoveAndCreateServiceItems(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) if approveErr != nil { logger.Fatal("Error approving move") } + _, _, primeErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID) + if primeErr != nil { + logger.Fatal("Error making move available to Prime") + } // AvailableToPrimeAt is set to the current time when a move is approved, we need to update it to fall within the // same contract as the rest of the timestamps on our move for pricing to work. @@ -4493,7 +4497,11 @@ func createHHGWithDestinationSITServiceItems(appCtx appcontext.AppContext, prime handlerConfig := handlers.Config{} ppmEstimator := ppmshipment.NewEstimatePPM(handlerConfig.DTODPlanner(), &paymentrequesthelper.RequestPaymentHelper{}) mtoUpdater := movetaskorder.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, signedCertificationCreator, signedCertificationUpdater, ppmEstimator) - _, approveErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) + _, approveErr := mtoUpdater.ApproveMoveAndCreateServiceItems(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) + _, _, primeErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID) + if primeErr != nil { + logger.Fatal("Error making move available to Prime") + } // AvailableToPrimeAt is set to the current time when a move is approved, we need to update it to fall within the // same contract as the rest of the timestamps on our move for pricing to work. @@ -4903,7 +4911,11 @@ func createHHGWithPaymentServiceItems( handlerConfig := handlers.Config{} ppmEstimator := ppmshipment.NewEstimatePPM(handlerConfig.DTODPlanner(), &paymentrequesthelper.RequestPaymentHelper{}) mtoUpdater := movetaskorder.NewMoveTaskOrderUpdater(queryBuilder, serviceItemCreator, moveRouter, signedCertificationCreator, signedCertificationUpdater, ppmEstimator) - _, approveErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) + _, approveErr := mtoUpdater.ApproveMoveAndCreateServiceItems(appCtx, move.ID, etag.GenerateEtag(move.UpdatedAt), true, true) + _, _, primeErr := mtoUpdater.MakeAvailableToPrime(appCtx, move.ID) + if primeErr != nil { + logger.Fatal("Error making move available to Prime") + } // AvailableToPrimeAt is set to the current time when a move is approved, we need to update it to fall within the // same contract as the rest of the timestamps on our move for pricing to work. diff --git a/scripts/rds-snapshot-app-db b/scripts/rds-snapshot-app-db index 6a3e1a704c6..ea2f0afd5c0 100755 --- a/scripts/rds-snapshot-app-db +++ b/scripts/rds-snapshot-app-db @@ -19,17 +19,35 @@ db_snapshot_identifier=$db_instance_identifier-$(date +%s) readonly db_snapshot_identifier readonly tags=("Key=Environment,Value=$environment" "Key=Tool,Value=$(basename "$0")") + echo echo "Wait for concurrent database snapshots for ${db_instance_identifier} to complete before continuing ..." -time aws rds wait db-snapshot-completed --db-instance-identifier "$db_instance_identifier" +time aws rds wait db-snapshot-completed --db-instance-identifier "$db_instance_identifier" + echo echo "Create database snapshot for ${db_instance_identifier} with identifier ${db_snapshot_identifier}" -aws rds create-db-snapshot --cli-read-timeout 0 --cli-connect-timeout 0 --db-instance-identifier "$db_instance_identifier" --db-snapshot-identifier "$db_snapshot_identifier" --tags "${tags[@]}" + + +aws rds create-db-snapshot --db-instance-identifier "$db_instance_identifier" --db-snapshot-identifier "$db_snapshot_identifier" --tags "${tags[@]}" + +#we want to loop while status is not available; check after initiating the create +while true; do + db_description=$(aws rds describe-db-snapshots --db-snapshot-identifier "$db_snapshot_identifier") + db_status=$(echo "${db_description}" | jq -r ".DBSnapshots[].Status") + echo "${db_snapshot_identifier} -- ${db_status}" + if [[ "${db_status}" == "available" ]]; then + break + fi + sleep 15 +done + + +#unnecessary confirm but will leave in echo echo "Wait for current database snapshot ${db_snapshot_identifier} to complete before continuing ..." -time aws rds wait db-snapshot-completed --db-snapshot-identifier "$db_snapshot_identifier" +time aws rds wait db-snapshot-completed --db-snapshot-identifier "$db_snapshot_identifier" echo echo "Describe the database snapshot ${db_snapshot_identifier}" diff --git a/scripts/run-server-test b/scripts/run-server-test index 0ead8674b81..45e8ab87afe 100755 --- a/scripts/run-server-test +++ b/scripts/run-server-test @@ -97,8 +97,15 @@ function server_report_cleanup() # https://unix.stackexchange.com/questions/305190/remove-last-character-from-string-captured-with-awk percent=$(grep '(statements)' "${test_dir}/go-coverage.txt" | awk '{print substr($NF, 1, length($NF)-1)}') goal_percent=50 + + #check if gitlab uses python3 + if [[ "${GITLAB:-}" == "1" ]]; then + python_exe="python3" + else + python_exe="python" + fi # using a oneline python function to test if percent is less than goal and return a proper exit code - if python -c "exit(1) if ${percent} < ${goal_percent} else exit()"; then + if exec "$python_exe" -c "exit(1) if ${percent} < ${goal_percent} else exit()"; then # coverage is good echo "total coverage is ${percent}%" else diff --git a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx index e1fb7f72263..d8e04da7494 100644 --- a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx +++ b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx @@ -314,12 +314,14 @@ class MtoShipmentForm extends Component {

{shipmentForm.header[`${shipmentType}`]}

- Remember: You can move + Remember: {isUB - ? ` up to ${formatUBAllowanceWeight( + ? ` You can move up to ${formatUBAllowanceWeight( orders?.entitlement?.ub_allowance, )} for this UB shipment. The weight of your UB is part of your authorized weight allowance` - : ` ${formatWeight(orders.authorizedWeight)} total`} + : ` Your standard weight allowance is ${formatWeight( + orders.authorizedWeight, + )} total. If you are moving to an administratively restricted HHG weight location this amount may be less`} . You’ll be billed for any excess weight you move.
diff --git a/src/components/Customer/PPM/Booking/EstimatedWeightsProGearForm/EstimatedWeightsProGearForm.jsx b/src/components/Customer/PPM/Booking/EstimatedWeightsProGearForm/EstimatedWeightsProGearForm.jsx index b83ed79aea2..98fb38d22ec 100644 --- a/src/components/Customer/PPM/Booking/EstimatedWeightsProGearForm/EstimatedWeightsProGearForm.jsx +++ b/src/components/Customer/PPM/Booking/EstimatedWeightsProGearForm/EstimatedWeightsProGearForm.jsx @@ -53,9 +53,9 @@ const EstimatedWeightsProGearForm = ({ orders, mtoShipment, onSubmit, onBack }) return (
- {`Total weight allowance for your move: ${formatWeight( + {`Remember: Your standard weight allowance is: ${formatWeight( weightAuthorized, - )}`} + )}. If you are moving to an administratively restricted HHG weight location this amount may be less. You will not be reimbursed for any excess weight you move.`}

PPM

diff --git a/src/components/Office/AddOrdersForm/AddOrdersForm.jsx b/src/components/Office/AddOrdersForm/AddOrdersForm.jsx index 113d8761284..a2c6be4ff4d 100644 --- a/src/components/Office/AddOrdersForm/AddOrdersForm.jsx +++ b/src/components/Office/AddOrdersForm/AddOrdersForm.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; import { Field, Formik } from 'formik'; import * as Yup from 'yup'; import { FormGroup, Label, Radio, Link as USWDSLink } from '@trussworks/react-uswds'; @@ -20,6 +21,7 @@ import Callout from 'components/Callout'; import MaskedTextField from 'components/form/fields/MaskedTextField/MaskedTextField'; import formStyles from 'styles/form.module.scss'; import ConnectedFlashMessage from 'containers/FlashMessage/FlashMessage'; +import { showCounselingOffices } from 'services/ghcApi'; let originMeta; let newDutyMeta = ''; @@ -32,6 +34,7 @@ const AddOrdersForm = ({ isBluebarkMoveSelected, }) => { const payGradeOptions = dropdownInputOptions(ORDERS_PAY_GRADE_OPTIONS); + const [counselingOfficeOptions, setCounselingOfficeOptions] = useState(null); const [currentDutyLocation, setCurrentDutyLocation] = useState(''); const [newDutyLocation, setNewDutyLocation] = useState(''); const [showAccompaniedTourField, setShowAccompaniedTourField] = useState(false); @@ -42,6 +45,7 @@ const AddOrdersForm = ({ const [isHasDependentsDisabled, setHasDependentsDisabled] = useState(false); const [prevOrderType, setPrevOrderType] = useState(''); const [filteredOrderTypeOptions, setFilteredOrderTypeOptions] = useState(ordersTypeOptions); + const { customerId: serviceMemberId } = useParams(); const validationSchema = Yup.object().shape({ ordersType: Yup.mixed() @@ -55,6 +59,9 @@ const AddOrdersForm = ({ .required('Required'), hasDependents: Yup.mixed().oneOf(['yes', 'no']).required('Required'), originDutyLocation: Yup.object().nullable().required('Required'), + counselingOfficeId: currentDutyLocation.provides_services_counseling + ? Yup.string().required('Required') + : Yup.string().notRequired(), newDutyLocation: Yup.object().nullable().required('Required'), grade: Yup.mixed().oneOf(Object.keys(ORDERS_PAY_GRADE_OPTIONS)).required('Required'), accompaniedTour: showAccompaniedTourField @@ -79,6 +86,17 @@ const AddOrdersForm = ({ }, []); useEffect(() => { + if (currentDutyLocation?.id && serviceMemberId) { + showCounselingOffices(currentDutyLocation.id, serviceMemberId).then((fetchedData) => { + if (fetchedData.body) { + const counselingOffices = fetchedData.body.map((item) => ({ + key: item.id, + value: item.name, + })); + setCounselingOfficeOptions(counselingOffices); + } + }); + } // Check if either currentDutyLocation or newDutyLocation is OCONUS if (currentDutyLocation?.address?.isOconus || newDutyLocation?.address?.isOconus) { setIsOconusMove(true); @@ -96,7 +114,7 @@ const AddOrdersForm = ({ setShowDependentAgeFields(false); } } - }, [currentDutyLocation, newDutyLocation, isOconusMove, hasDependents, enableUB]); + }, [currentDutyLocation, newDutyLocation, isOconusMove, hasDependents, enableUB, serviceMemberId]); useEffect(() => { const fetchData = async () => { @@ -115,11 +133,19 @@ const AddOrdersForm = ({ return ( - {({ values, isValid, isSubmitting, handleSubmit, handleChange, touched, setFieldValue }) => { + {({ values, isValid, isSubmitting, handleSubmit, handleChange, touched, setFieldValue, setValues }) => { const isRetirementOrSeparation = ['RETIREMENT', 'SEPARATION'].includes(values.ordersType); if (!values.origin_duty_location && touched.origin_duty_location) originMeta = 'Required'; else originMeta = null; + const handleCounselingOfficeChange = () => { + setValues({ + ...values, + counselingOfficeId: null, + }); + setCounselingOfficeOptions(null); + }; + if (!values.newDutyLocation && touched.newDutyLocation) newDutyMeta = 'Required'; else newDutyMeta = null; const handleHasDependentsChange = (e) => { @@ -172,9 +198,10 @@ const AddOrdersForm = ({ handleOrderTypeChange(e); }} isDisabled={isSafetyMoveSelected || isBluebarkMoveSelected} + hint="Required" /> - - + + { setCurrentDutyLocation(e); + handleCounselingOfficeChange(); }} metaOverride={originMeta} required + hint="Required" /> + {currentDutyLocation.provides_services_counseling && ( +

+ + +
+ )} {isRetirementOrSeparation ? ( <> @@ -216,6 +263,7 @@ const AddOrdersForm = ({ displayAddress={false} placeholder="Enter a city or ZIP" metaOverride={newDutyMeta} + hint="Required" onDutyLocationChange={(e) => { setNewDutyLocation(e); }} @@ -226,6 +274,7 @@ const AddOrdersForm = ({ name="newDutyLocation" label="New duty location" required + hint="Required" metaOverride={newDutyMeta} onDutyLocationChange={(e) => { setNewDutyLocation(e); @@ -234,7 +283,7 @@ const AddOrdersForm = ({ )} - +
- +
)} - +
diff --git a/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx b/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx index 43c59015235..51f420ebfb0 100644 --- a/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx +++ b/src/components/Office/AddOrdersForm/AddOrdersForm.test.jsx @@ -5,10 +5,12 @@ import { Provider } from 'react-redux'; import AddOrdersForm from './AddOrdersForm'; +import { MockProviders } from 'testUtils'; import { dropdownInputOptions } from 'utils/formatters'; import { ORDERS_TYPE, ORDERS_TYPE_OPTIONS } from 'constants/orders'; import { configureStore } from 'shared/store'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; +import { servicesCounselingRoutes } from 'constants/routes'; jest.setTimeout(60000); @@ -75,10 +77,44 @@ jest.mock('components/LocationSearchBox/api', () => ({ name: 'Luke AFB', updated_at: '2021-02-11T16:48:04.117Z', }, + { + address: { + city: '', + id: '25be4d12-fe93-47f1-bbec-1db386dfa67e', + postalCode: '', + state: '', + streetAddress1: '', + }, + address_id: '4334640b-c35e-4293-a2f1-36c7b629f904', + affiliation: 'AIR_FORCE', + created_at: '2021-02-11T16:48:04.117Z', + id: '22f0755f-6f35-478b-9a75-35a69211da1d', + name: 'Scott AFB', + updated_at: '2021-02-11T16:48:04.117Z', + provides_services_counseling: true, + }, ]), ), })); +jest.mock('services/ghcApi', () => ({ + ...jest.requireActual('services/ghcApi'), + showCounselingOffices: jest.fn().mockImplementation(() => + Promise.resolve({ + body: [ + { + id: '3e937c1f-5539-4919-954d-017989130584', + name: 'Albuquerque AFB', + }, + { + id: 'fa51dab0-4553-4732-b843-1f33407f77bc', + name: 'Glendale Luke AFB', + }, + ], + }), + ), +})); + jest.mock('utils/featureFlags', () => ({ ...jest.requireActual('utils/featureFlags'), isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)), @@ -96,6 +132,7 @@ const initialValues = { accompaniedTour: '', dependentsUnderTwelve: '', dependentsTwelveAndOver: '', + counselingOfficeId: '', }; const testProps = { initialValues, @@ -103,6 +140,8 @@ const testProps = { onSubmit: jest.fn(), onBack: jest.fn(), }; +const mockParams = { customerId: 'ea51dab0-4553-4732-b843-1f33407f77bd' }; +const mockPath = servicesCounselingRoutes.BASE_CUSTOMERS_ORDERS_ADD_PATH; describe('CreateMoveCustomerInfo Component', () => { it('renders the form inputs', async () => { @@ -114,15 +153,15 @@ describe('CreateMoveCustomerInfo Component', () => { await waitFor(() => { expect(screen.getByText('Tell us about the orders')).toBeInTheDocument(); - expect(screen.getByLabelText('Orders type')).toBeInTheDocument(); - expect(screen.getByLabelText('Orders date')).toBeInTheDocument(); - expect(screen.getByLabelText('Report by date')).toBeInTheDocument(); - expect(screen.getByText('Are dependents included in the orders?')).toBeInTheDocument(); + expect(screen.getByLabelText(/Orders type/)).toBeInTheDocument(); + expect(screen.getByLabelText(/Orders date/)).toBeInTheDocument(); + expect(screen.getByLabelText(/Report by date/)).toBeInTheDocument(); + expect(screen.getByText(/Are dependents included in the orders?/)).toBeInTheDocument(); expect(screen.getByTestId('hasDependentsYes')).toBeInTheDocument(); expect(screen.getByTestId('hasDependentsNo')).toBeInTheDocument(); - expect(screen.getByLabelText('Current duty location')).toBeInTheDocument(); - expect(screen.getByLabelText('New duty location')).toBeInTheDocument(); - expect(screen.getByLabelText('Pay grade')).toBeInTheDocument(); + expect(screen.getByLabelText(/Current duty location/)).toBeInTheDocument(); + expect(screen.getByLabelText(/New duty location/)).toBeInTheDocument(); + expect(screen.getByLabelText(/Pay grade/)).toBeInTheDocument(); }); }); @@ -135,7 +174,7 @@ describe('CreateMoveCustomerInfo Component', () => { , ); - const ordersTypeDropdown = getByLabelText('Orders type'); + const ordersTypeDropdown = getByLabelText(/Orders type/); expect(ordersTypeDropdown).toBeInstanceOf(HTMLSelectElement); await userEvent.selectOptions(ordersTypeDropdown, ORDERS_TYPE.PERMANENT_CHANGE_OF_STATION); @@ -166,12 +205,12 @@ describe('CreateMoveCustomerInfo Component', () => { , ); - await userEvent.click(getByLabelText('Orders type')); - await userEvent.click(getByLabelText('Orders date')); - await userEvent.click(getByLabelText('Report by date')); - await userEvent.click(getByLabelText('Current duty location')); - await userEvent.click(getByLabelText('New duty location')); - await userEvent.click(getByLabelText('Pay grade')); + await userEvent.click(getByLabelText(/Orders type/)); + await userEvent.click(getByLabelText(/Orders date/)); + await userEvent.click(getByLabelText(/Report by date/)); + await userEvent.click(getByLabelText(/Current duty location/)); + await userEvent.click(getByLabelText(/New duty location/)); + await userEvent.click(getByLabelText(/Pay grade/)); const submitBtn = getByRole('button', { name: 'Next' }); await userEvent.click(submitBtn); @@ -191,7 +230,7 @@ describe('AddOrdersForm - OCONUS and Accompanied Tour Test', () => { it('submits the form with OCONUS values and accompanied tour selection', async () => { isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); render( - + , ); @@ -207,6 +246,9 @@ describe('AddOrdersForm - OCONUS and Accompanied Tour Test', () => { const selectedOptionCurrent = await screen.findByText(/Elmendorf/); await userEvent.click(selectedOptionCurrent); + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeFalsy(); + // Test New Duty Location Search Box interaction await userEvent.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 100 }); const selectedOptionNew = await screen.findByText(/Luke/); @@ -239,7 +281,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = , ); - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.STUDENT_TRAVEL); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.STUDENT_TRAVEL); const hasDependentsYes = screen.getByLabelText('Yes'); const hasDependentsNo = screen.getByLabelText('No'); @@ -260,7 +302,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = , ); - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.EARLY_RETURN_OF_DEPENDENTS); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.EARLY_RETURN_OF_DEPENDENTS); const hasDependentsYes = screen.getByLabelText('Yes'); const hasDependentsNo = screen.getByLabelText('No'); @@ -280,7 +322,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = , ); - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.PERMANENT_CHANGE_OF_STATION); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.PERMANENT_CHANGE_OF_STATION); const hasDependentsYesPermChg = screen.getByLabelText('Yes'); const hasDependentsNoPermChg = screen.getByLabelText('No'); @@ -293,7 +335,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = }); // set order type to value that disables and defaults "has dependents" - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.STUDENT_TRAVEL); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.STUDENT_TRAVEL); const hasDependentsYesStudent = screen.getByLabelText('Yes'); const hasDependentsNoStudent = screen.getByLabelText('No'); @@ -305,7 +347,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = }); // set order type to value the re-enables "has dependents" - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.LOCAL_MOVE); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.LOCAL_MOVE); const hasDependentsYesLocalMove = screen.getByLabelText('Yes'); const hasDependentsNoLocalMove = screen.getByLabelText('No'); @@ -327,7 +369,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = , ); - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.PERMANENT_CHANGE_OF_STATION); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.PERMANENT_CHANGE_OF_STATION); const hasDependentsYesPermChg = screen.getByLabelText('Yes'); const hasDependentsNoPermChg = screen.getByLabelText('No'); @@ -340,7 +382,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = }); // set order type to value that disables and defaults "has dependents" - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.EARLY_RETURN_OF_DEPENDENTS); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.EARLY_RETURN_OF_DEPENDENTS); const hasDependentsYesEarly = screen.getByLabelText('Yes'); const hasDependentsNoEarly = screen.getByLabelText('No'); @@ -352,7 +394,7 @@ describe('AddOrdersForm - Student Travel, Early Return of Dependents Test', () = }); // set order type to value the re-enables "has dependents" - await userEvent.selectOptions(screen.getByLabelText('Orders type'), ORDERS_TYPE.LOCAL_MOVE); + await userEvent.selectOptions(screen.getByLabelText(/Orders type/), ORDERS_TYPE.LOCAL_MOVE); const hasDependentsYesLocalMove = screen.getByLabelText('Yes'); const hasDependentsNoLocalMove = screen.getByLabelText('No'); @@ -386,3 +428,70 @@ describe('AddOrdersForm - Edge Cases and Additional Scenarios', () => { expect(screen.getByLabelText(/Orders type/)).toBeDisabled(); }); }); + +describe('AddOrdersForm - With Counseling Office', () => { + it('displays the counseling office dropdown', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + render( + + + , + ); + + await userEvent.selectOptions(await screen.findByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await userEvent.paste(screen.getByLabelText(/Orders date/), '08 Nov 2020'); + await userEvent.paste(screen.getByLabelText(/Report by date/), '26 Nov 2020'); + await userEvent.click(screen.getByLabelText('No')); + await userEvent.selectOptions(screen.getByLabelText(/Pay grade/), ['E_5']); + + // Test Current Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 100 }); + const selectedOptionCurrent = await screen.findByText(/Scott/); + await userEvent.click(selectedOptionCurrent); + + // Test New Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 100 }); + const selectedOptionNew = await screen.findByText(/Luke/); + await userEvent.click(selectedOptionNew); + + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeTruthy(); + + await userEvent.selectOptions(screen.getByLabelText(/Counseling office/), ['Albuquerque AFB']); + + const nextBtn = screen.getByRole('button', { name: 'Next' }); + expect(nextBtn.getAttribute('disabled')).toBeFalsy(); + }); + + it('disabled submit if counseling office is required and blank', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + render( + + + , + ); + + await userEvent.selectOptions(await screen.findByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await userEvent.type(screen.getByLabelText(/Orders date/), '08 Nov 2024'); + await userEvent.type(screen.getByLabelText(/Report by date/), '26 Nov 2024'); + + // Test Current Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 100 }); + const selectedOptionCurrent = await screen.findByText(/Scott/); + await userEvent.click(selectedOptionCurrent); + + // Test New Duty Location Search Box interaction + await userEvent.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 100 }); + const selectedOptionNew = await screen.findByText(/Luke/); + await userEvent.click(selectedOptionNew); + + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeTruthy(); // If the field is visible then it it required + + await userEvent.selectOptions(screen.getByLabelText(/Pay grade/), ['E_5']); + await userEvent.click(screen.getByLabelText('No')); + + const nextBtn = await screen.getByRole('button', { name: 'Next' }, { delay: 100 }); + expect(nextBtn).toBeDisabled(); + }); +}); diff --git a/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.jsx b/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.jsx index 94d8bcc2dc3..7b38d00bb68 100644 --- a/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.jsx +++ b/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; +import { useFormikContext } from 'formik'; import { isBooleanFlagEnabled } from '../../../utils/featureFlags'; import { FEATURE_FLAG_KEYS } from '../../../shared/constants'; @@ -20,6 +21,8 @@ const AllowancesDetailForm = ({ header, entitlements, branchOptions, formIsDisab entitlements?.dependentsTwelveAndOver || entitlements?.dependentsUnderTwelve ); + const { setFieldValue } = useFormikContext(); + const [isAdminWeightLocationChecked, setIsAdminWeightLocationChecked] = useState(entitlements?.weightRestriction > 0); useEffect(() => { // Functional component version of "componentDidMount" // By leaving the dependency array empty this will only run once @@ -32,6 +35,25 @@ const AllowancesDetailForm = ({ header, entitlements, branchOptions, formIsDisab checkUBFeatureFlag(); }, []); + useEffect(() => { + if (!isAdminWeightLocationChecked) { + // Find the weight restriction input and reset its value to 0 + const weightRestrictionInput = document.getElementById('weightRestrictionId'); + if (weightRestrictionInput) { + weightRestrictionInput.value = ''; + } + } + }, [isAdminWeightLocationChecked]); + + const handleAdminWeightLocationChange = (e) => { + const isChecked = e.target.checked; + setIsAdminWeightLocationChecked(isChecked); + + if (!isChecked) { + setFieldValue('weightRestriction', ''); + } + }; + return (
{header &&

{header}

} @@ -146,6 +168,10 @@ const AllowancesDetailForm = ({ header, entitlements, branchOptions, formIsDisab lazy={false} // immediate masking evaluation isDisabled={formIsDisabled} /> +
+
Standard weight allowance
+
{formatWeight(entitlements.totalWeight)}
+
-
-
Weight allowance
-
{formatWeight(entitlements.totalWeight)}
-
+
+ +
+ {isAdminWeightLocationChecked && ( + + )}
); }; - AllowancesDetailForm.propTypes = { entitlements: EntitlementShape.isRequired, branchOptions: DropdownArrayOf.isRequired, diff --git a/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.test.jsx b/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.test.jsx index 503ab865df3..fcf825bd404 100644 --- a/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.test.jsx +++ b/src/components/Office/AllowancesDetailForm/AllowancesDetailForm.test.jsx @@ -159,3 +159,40 @@ describe('AllowancesDetailForm', () => { expect(screen.queryByLabelText(/Number of dependents of the age 12 or over/)).toBeInTheDocument(); }); }); +describe('AllowancesDetailForm additional tests', () => { + it('renders gun safe checkbox field', async () => { + render( + + + , + ); + + expect(await screen.findByTestId('gunSafeInput')).toBeInTheDocument(); + }); + + it('renders admin weight location section with conditional weight restriction field', async () => { + render( + + + , + ); + + const adminWeightCheckbox = await screen.findByTestId('adminWeightLocation'); + expect(adminWeightCheckbox).toBeInTheDocument(); + expect(screen.queryByTestId('weightRestrictionInput')).not.toBeInTheDocument(); + await act(async () => { + adminWeightCheckbox.click(); + }); + expect(screen.getByTestId('weightRestrictionInput')).toBeInTheDocument(); + }); + + it('displays the total weight allowance correctly', async () => { + render( + + + , + ); + + expect(await screen.findByTestId('weightAllowance')).toHaveTextContent('11,000'); + }); +}); diff --git a/src/components/Office/DefinitionLists/AllowancesList.jsx b/src/components/Office/DefinitionLists/AllowancesList.jsx index 3d5c1e850cc..7bdd17862ae 100644 --- a/src/components/Office/DefinitionLists/AllowancesList.jsx +++ b/src/components/Office/DefinitionLists/AllowancesList.jsx @@ -16,7 +16,6 @@ const AllowancesList = ({ info, showVisualCues }) => { const visualCuesStyle = classNames(descriptionListStyles.row, { [`${descriptionListStyles.rowWithVisualCue}`]: showVisualCues, }); - useEffect(() => { const checkUBFeatureFlag = async () => { const enabled = await isBooleanFlagEnabled(FEATURE_FLAG_KEYS.UNACCOMPANIED_BAGGAGE); @@ -35,7 +34,7 @@ const AllowancesList = ({ info, showVisualCues }) => {
{info.branch ? ORDERS_BRANCH_OPTIONS[info.branch] : ''}
-
Weight allowance
+
Standard weight allowance
{formatWeight(info.totalWeight)}
@@ -104,11 +103,21 @@ const AllowancesList = ({ info, showVisualCues }) => {
Gun Safe
{info.gunSafe ? 'Authorized' : 'Unauthorized'}
+
+
Admin Weight Restricted Location
+
{info.weightRestriction > 0 ? 'Yes' : 'No'}
+
+ +
+
Weight Restriction
+
+ {info.weightRestriction ? formatWeight(info.weightRestriction) : DEFAULT_EMPTY_VALUE} +
+
); }; - AllowancesList.propTypes = { info: PropTypes.shape({ branch: PropTypes.string, diff --git a/src/components/Office/DefinitionLists/AllowancesList.test.jsx b/src/components/Office/DefinitionLists/AllowancesList.test.jsx index 45c2e97f246..9eed73f1d62 100644 --- a/src/components/Office/DefinitionLists/AllowancesList.test.jsx +++ b/src/components/Office/DefinitionLists/AllowancesList.test.jsx @@ -19,6 +19,7 @@ const info = { requiredMedicalEquipmentWeight: 1000, organizationalClothingAndIndividualEquipment: true, ubAllowance: 400, + weightRestriction: 1500, }; const initialValuesOconusAdditions = { @@ -175,4 +176,9 @@ describe('AllowancesList', () => { expect(screen.getByTestId('unaccompaniedBaggageAllowance')).toBeInTheDocument(); expect(screen.getByTestId('unaccompaniedBaggageAllowance').textContent).toEqual('400 lbs'); }); + it('renders weight restriction', () => { + const adminRestrictedWtLoc = { ...info, adminRestrictedWeightLocation: true }; + render(); + expect(screen.getByTestId('weightRestriction').textContent).toEqual('1,500 lbs'); + }); }); diff --git a/src/components/Office/DefinitionLists/OrdersList.jsx b/src/components/Office/DefinitionLists/OrdersList.jsx index 46ec027d40e..27de47eb1fc 100644 --- a/src/components/Office/DefinitionLists/OrdersList.jsx +++ b/src/components/Office/DefinitionLists/OrdersList.jsx @@ -15,7 +15,7 @@ import { ordersTypeDetailReadable, } from 'utils/formatters'; -const OrdersList = ({ ordersInfo, showMissingWarnings }) => { +const OrdersList = ({ ordersInfo, moveInfo, showMissingWarnings }) => { const { ordersType } = ordersInfo; const isRetiree = ordersType === 'RETIREMENT'; const isSeparatee = ordersType === 'SEPARATION'; @@ -57,6 +57,12 @@ const OrdersList = ({ ordersInfo, showMissingWarnings }) => {
Current duty location
{ordersInfo.currentDutyLocation?.name}
+
+
Counseling office
+
+ {moveInfo?.counselingOffice?.name ? moveInfo.counselingOffice.name : '—'} +
+
{ {isRetiree || isSeparatee ? 'HOR, HOS, or PLEAD' : 'New duty location'}
- {ordersInfo.newDutyLocation?.name ? ordersInfo.newDutyLocation?.name : '-'} + {ordersInfo.newDutyLocation?.name ? ordersInfo.newDutyLocation?.name : '—'}
( NTStac: text('ordersInfo.NTStac', '9999'), payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} />
); @@ -81,6 +84,9 @@ export const AsServiceCounselor = () => ( NTStac: '', payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} />
); @@ -105,6 +111,9 @@ export const AsServiceCounselorProcessingRetirement = () => ( NTStac: '', payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} />
); @@ -129,6 +138,9 @@ export const AsServiceCounselorProcessingSeparation = () => ( NTStac: '', payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} />
); @@ -166,6 +178,9 @@ export const AsTOO = () => ( NTStac: '', payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} />
); @@ -212,6 +227,9 @@ export const AsTOOProcessingSeparation = () => ( NTStac: '', payGrade: text('ordersInfo.payGrade', 'E_5'), }} + moveInfo={{ + name: 'PPPO Los Angeles SFB - USAF', + }} /> ); diff --git a/src/components/Office/DefinitionLists/OrdersList.test.jsx b/src/components/Office/DefinitionLists/OrdersList.test.jsx index 586c0d1bfab..107463b7a1c 100644 --- a/src/components/Office/DefinitionLists/OrdersList.test.jsx +++ b/src/components/Office/DefinitionLists/OrdersList.test.jsx @@ -32,9 +32,16 @@ const ordersInfo = { payGrade: 'E_7', }; +const moveInfo = { + counselingOffice: { + name: 'PPPO Los Angeles SFB - USAF', + }, +}; + // what ordersInfo from above should be rendered as const expectedRenderedOrdersInfo = { currentDutyLocation: 'JBSA Lackland', + counselingOffice: 'PPPO Los Angeles SFB - USAF', newDutyLocation: 'JB Lewis-McChord', issuedDate: '08 Mar 2020', reportByDate: '01 Apr 2020', @@ -65,7 +72,7 @@ const ordersInfoMissing = { describe('OrdersList', () => { it('renders formatted orders info', () => { - render(); + render(); Object.keys(expectedRenderedOrdersInfo).forEach((key) => { expect(screen.getByText(expectedRenderedOrdersInfo[key])).toBeInTheDocument(); }); diff --git a/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx b/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx index 6830db6b735..b50fbc53e2f 100644 --- a/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx +++ b/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx @@ -140,6 +140,7 @@ ShipmentInfoListSelector.propTypes = { SHIPMENT_TYPES.BOAT_HAUL_AWAY, SHIPMENT_TYPES.BOAT_TOW_AWAY, SHIPMENT_OPTIONS.MOBILE_HOME, + SHIPMENT_OPTIONS.UNACCOMPANIED_BAGGAGE, ]), isForEvaluationReport: PropTypes.bool, destinationDutyLocationPostalCode: PropTypes.string, diff --git a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx index 585ef6b44d6..e6cbba78392 100644 --- a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx +++ b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.jsx @@ -6,7 +6,7 @@ import classnames from 'classnames'; import styles from './ExpandableServiceItemRow.module.scss'; import { PAYMENT_SERVICE_ITEM_STATUS } from 'shared/constants'; -import { allowedServiceItemCalculations } from 'constants/serviceItems'; +import { allowedServiceItemCalculations, SERVICE_ITEM_CODES } from 'constants/serviceItems'; import { PaymentServiceItemShape } from 'types'; import { MTOServiceItemShape } from 'types/order'; import { toDollarString, formatCents, formatDollarFromMillicents } from 'utils/formatters'; @@ -25,7 +25,8 @@ const ExpandableServiceItemRow = ({ return canShowExpandableContent && (paymentIsDeprecated || item.status !== PAYMENT_SERVICE_ITEM_STATUS.REQUESTED); }; const canShowExpandableContent = - !disableExpansion && allowedServiceItemCalculations.includes(serviceItem.mtoServiceItemCode); + !disableExpansion && + (allowedServiceItemCalculations.includes(serviceItem.mtoServiceItemCode) || serviceItem.rejectionReason); const handleExpandClick = () => { setIsExpanded((prev) => !prev); @@ -43,6 +44,11 @@ const ExpandableServiceItemRow = ({ [styles.expandedDetail]: isExpanded, }); + const colSpan = + serviceItem.mtoServiceItemCode === SERVICE_ITEM_CODES.MS || serviceItem.mtoServiceItemCode === SERVICE_ITEM_CODES.CS + ? 4 + : 2; + return ( <> {isExpanded && ( - - - + {Object.keys(additionalServiceItemData).length > 0 && ( + + + + )} + {serviceItem.rejectionReason && ( + +
+ +

Rejection Reason

+
+ {serviceItem.rejectionReason} +
+ + )} )} diff --git a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.module.scss b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.module.scss index 49a76c0f3b9..c6ce0d63ff3 100644 --- a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.module.scss +++ b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.module.scss @@ -3,6 +3,7 @@ @import 'shared/styles/colors'; .ExpandableServiceItemRow { + background-color: $bg-gray; .accepted { svg { @include u-margin-right(1); @@ -72,4 +73,56 @@ &.expandedDetail td { padding: 0; } + + .rejectionReasonCol { + @include u-padding(2); + border: 1px solid $base-lighter; + border-radius: 3px; + background-color: $bg-gray; + } + + .title { + font-weight: bold; + margin-top: 0; + @include u-margin-bottom(2); + @include u-padding-left(1); + @include u-padding-top(2); + } + + .reasonText { + font-weight: normal; + } + + td[colspan='2']:nth-of-type(2).rejectionReasonTd { + vertical-align: top; + text-align: start; + word-break: break-word; + padding-right: 2rem; + padding-left: 1rem; + border-left: 1px solid #dfe1e2; + background-color: $bg-gray; + } + + .rejectionReasonContainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + padding-left: 1rem; + padding-bottom: 1rem; + padding-right: 1rem; + background-color: $bg-gray; + + svg { + path { + fill: $error; + } + height: 1.4rem; + } + } + + .break { + flex-basis: 100%; + height: 0; + } } diff --git a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.stories.jsx b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.stories.jsx new file mode 100644 index 00000000000..4245c20706c --- /dev/null +++ b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.stories.jsx @@ -0,0 +1,235 @@ +import React from 'react'; +import { GridContainer } from '@trussworks/react-uswds'; + +import ExpandableServiceItemRow from './ExpandableServiceItemRow'; +import '../ServiceItemCalculations/ServiceItemCalculations.module.scss'; + +export default { + title: 'Office Components/ExpandableServiceItemRow', + decorators: [ + (Story) => { + return ( +
+ + + + + + + + + + + + + + + + + +
Service itemAmountStatus
+
+
+ ); + }, + ], +}; + +const serviceItemRejected = { + createdAt: '2025-01-09T22:08:38.788Z', + eTag: 'MjAyNS0wMS0xN1QxNTowODo0Mi44MDI4MDZa', + id: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + mtoServiceItemCode: 'DLH', + mtoServiceItemID: '526f705d-dba1-4bae-bf9a-e97cd1931bd4', + mtoServiceItemName: 'Domestic linehaul', + mtoShipmentID: 'ad5c56af-9e32-41bf-8283-a6a52938cc6a', + mtoShipmentType: 'HHG', + paymentServiceItemParams: [ + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDQ5Nlo=', + id: 'd3fba800-cc16-45e3-975d-3236884fbf8a', + key: 'WeightOriginal', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '2000', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDAyNjFa', + id: '19192fe0-3e0b-4d5d-98dd-ea834fe9062f', + key: 'ActualPickupDate', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-09', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MjcwNTZa', + id: 'e249d609-96fa-4533-90dc-12d3164aed41', + key: 'RequestedPickupDate', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-02', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NDc5NjVa', + id: '75fc3b3b-517d-4383-9d3e-1493bcd564d9', + key: 'DistanceZip', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1540', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NTk0MTZa', + id: '15edbab6-ba00-47e2-bc92-49c7b16f57e1', + key: 'ContractYearName', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: 'Award Term 1', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDIwMVo=', + id: '984fb8ea-da8a-4b3a-8560-85a9da1589ab', + key: 'ZipDestAddress', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '85309', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDk1MzNa', + id: '6d8b063b-8af4-4d6a-9ad4-5880a2d5fbea', + key: 'ReferenceDate', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-02', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MzY0MjJa', + id: '7941f3cf-7bcd-495e-9291-f2965291676c', + key: 'ServiceAreaOrigin', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '456', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjExODda', + id: '9a69db91-2d5d-4446-b1f6-0c15a140cf7a', + key: 'EscalationCompounded', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DECIMAL', + value: '1.10701', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjI5ODla', + id: '35ddc999-a546-4076-9a2e-5295e4ae4279', + key: 'IsPeak', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'BOOLEAN', + value: 'false', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjQ3ODNa', + id: 'd35e3639-6a43-4f6a-8bf0-5a82685b80c4', + key: 'PriceRateOrFactor', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DECIMAL', + value: '3.148', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44Mzg0MTRa', + id: '163babcf-7874-474e-a36d-f602fdca5c88', + key: 'ContractCode', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: 'TRUSS_TEST', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44Mjk2OTFa', + id: '3f677c1e-1302-4cc1-9333-6fa34f5bdab5', + key: 'WeightEstimated', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1500', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDc2NDNa', + id: '1009cca9-cdc1-4814-97e9-e0ef82dce965', + key: 'WeightBilled', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1650', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MzE2Nzha', + id: '66b571ee-4142-4576-b563-cc3c8ea04bfe', + key: 'ZipPickupAddress', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '62225', + }, + ], + priceCents: 8855385, + referenceID: '4131-9325-46e2df6f', + rejectionReason: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + status: 'DENIED', +}; + +const additionalServiceItemData = { + approvedAt: '2025-01-09T20:24:58.522Z', + convertToCustomerExpense: false, + createdAt: '2025-01-09T20:24:58.621Z', + deletedAt: '0001-01-01', + eTag: 'MjAyNS0wMS0wOVQyMDoyNDo1OC42MjE5NzRa', + id: '526f705d-dba1-4bae-bf9a-e97cd1931bd4', + moveTaskOrderID: 'b02c42d7-bd4f-48ff-a5f8-6e7332fa5d03', + mtoShipmentID: 'ad5c56af-9e32-41bf-8283-a6a52938cc6a', + reServiceCode: 'DLH', + reServiceID: '8d600f25-1def-422d-b159-617c7d59156e', + reServiceName: 'Domestic linehaul', + status: 'APPROVED', + submittedAt: '0001-01-01', + updatedAt: '0001-01-01T00:00:00.000Z', +}; + +export const rejectedServiceItem = () => ( + +); + +const serviceItemAccepted = { ...serviceItemRejected }; +serviceItemAccepted.status = 'APPROVED'; +serviceItemAccepted.rejectionReason = null; +export const acceptedServiceItem = () => ( + +); diff --git a/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.test.jsx b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.test.jsx new file mode 100644 index 00000000000..2bc115f4586 --- /dev/null +++ b/src/components/Office/ExpandableServiceItemRow/ExpandableServiceItemRow.test.jsx @@ -0,0 +1,301 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; + +import ExpandableServiceItemRow from './ExpandableServiceItemRow'; + +import { SERVICE_ITEM_CODES } from 'constants/serviceItems'; + +const serviceItem = { + createdAt: '2025-01-09T22:08:38.788Z', + eTag: 'MjAyNS0wMS0xN1QxNTowODo0Mi44MDI4MDZa', + id: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + mtoServiceItemCode: 'DLH', + mtoServiceItemID: '526f705d-dba1-4bae-bf9a-e97cd1931bd4', + mtoServiceItemName: 'Domestic linehaul', + mtoShipmentID: 'ad5c56af-9e32-41bf-8283-a6a52938cc6a', + mtoShipmentType: 'HHG', + paymentServiceItemParams: [ + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDQ5Nlo=', + id: 'd3fba800-cc16-45e3-975d-3236884fbf8a', + key: 'WeightOriginal', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '2000', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDAyNjFa', + id: '19192fe0-3e0b-4d5d-98dd-ea834fe9062f', + key: 'ActualPickupDate', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-09', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MjcwNTZa', + id: 'e249d609-96fa-4533-90dc-12d3164aed41', + key: 'RequestedPickupDate', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-02', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NDc5NjVa', + id: '75fc3b3b-517d-4383-9d3e-1493bcd564d9', + key: 'DistanceZip', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1540', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NTk0MTZa', + id: '15edbab6-ba00-47e2-bc92-49c7b16f57e1', + key: 'ContractYearName', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: 'Award Term 1', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDIwMVo=', + id: '984fb8ea-da8a-4b3a-8560-85a9da1589ab', + key: 'ZipDestAddress', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '85309', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDk1MzNa', + id: '6d8b063b-8af4-4d6a-9ad4-5880a2d5fbea', + key: 'ReferenceDate', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DATE', + value: '2025-01-02', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MzY0MjJa', + id: '7941f3cf-7bcd-495e-9291-f2965291676c', + key: 'ServiceAreaOrigin', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '456', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjExODda', + id: '9a69db91-2d5d-4446-b1f6-0c15a140cf7a', + key: 'EscalationCompounded', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DECIMAL', + value: '1.10701', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjI5ODla', + id: '35ddc999-a546-4076-9a2e-5295e4ae4279', + key: 'IsPeak', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'BOOLEAN', + value: 'false', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC45NjQ3ODNa', + id: 'd35e3639-6a43-4f6a-8bf0-5a82685b80c4', + key: 'PriceRateOrFactor', + origin: 'PRICER', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'DECIMAL', + value: '3.148', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44Mzg0MTRa', + id: '163babcf-7874-474e-a36d-f602fdca5c88', + key: 'ContractCode', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: 'TRUSS_TEST', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44Mjk2OTFa', + id: '3f677c1e-1302-4cc1-9333-6fa34f5bdab5', + key: 'WeightEstimated', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1500', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44NDc2NDNa', + id: '1009cca9-cdc1-4814-97e9-e0ef82dce965', + key: 'WeightBilled', + origin: 'SYSTEM', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'INTEGER', + value: '1650', + }, + { + eTag: 'MjAyNS0wMS0wOVQyMjowODozOC44MzE2Nzha', + id: '66b571ee-4142-4576-b563-cc3c8ea04bfe', + key: 'ZipPickupAddress', + origin: 'PRIME', + paymentServiceItemID: '46e2df6f-4fe9-47ee-9baa-b9de28251da8', + type: 'STRING', + value: '62225', + }, + ], + priceCents: 8855385, + referenceID: '4131-9325-46e2df6f', + rejectionReason: 'Rejection reason test text', + status: 'DENIED', +}; + +const additionalServiceItemData = { + approvedAt: '2025-01-09T20:24:58.522Z', + convertToCustomerExpense: false, + createdAt: '2025-01-09T20:24:58.621Z', + deletedAt: '0001-01-01', + eTag: 'MjAyNS0wMS0wOVQyMDoyNDo1OC42MjE5NzRa', + id: '526f705d-dba1-4bae-bf9a-e97cd1931bd4', + moveTaskOrderID: 'b02c42d7-bd4f-48ff-a5f8-6e7332fa5d03', + mtoShipmentID: 'ad5c56af-9e32-41bf-8283-a6a52938cc6a', + reServiceCode: 'DLH', + reServiceID: '8d600f25-1def-422d-b159-617c7d59156e', + reServiceName: 'Domestic linehaul', + status: 'APPROVED', + submittedAt: '0001-01-01', + updatedAt: '0001-01-01T00:00:00.000Z', +}; + +const basicServiceItem = { ...serviceItem }; +basicServiceItem.mtoServiceItemCode = SERVICE_ITEM_CODES.MS; +basicServiceItem.mtoServiceItemName = 'Move Management'; + +describe('Payment service items', () => { + it('Displays rejected service item with rejection reason', async () => { + render( + , + ); + + expect(screen.getByText('Domestic linehaul')).toBeVisible(); + expect(screen.getByText('$88,553.85')).toBeVisible(); + expect(screen.getByText('Rejected')).toBeVisible(); + + // Expand service item row + await act(async () => { + screen.getByText('Domestic linehaul').click(); + }); + + // Test for rejection reason + expect(screen.getByText('Rejection Reason')).toBeVisible(); + expect(screen.getByText('Rejection reason test text')).toBeVisible(); + + // Test for calculations + expect(screen.getByText('Billable weight (cwt)')).toBeVisible(); + expect(screen.getByText('16.5 cwt')).toBeVisible(); + expect(screen.getByText('Original: 2,000 lbs')).toBeVisible(); + expect(screen.getByText('Estimated: 1,500 lbs')).toBeVisible(); + + expect(screen.getByText('Mileage')).toBeVisible(); + expect(screen.getByText('1,540')).toBeVisible(); + expect(screen.getByText('ZIP 62225 to ZIP 85309')).toBeVisible(); + + expect(screen.getByText('Baseline linehaul price')).toBeVisible(); + expect(screen.getByText('3.148')).toBeVisible(); + expect(screen.getByText('Domestic non-peak')).toBeVisible(); + expect(screen.getByText('Origin service area: 456')).toBeVisible(); + expect(screen.getByText('Requested pickup: 02 Jan 2025')).toBeVisible(); + + expect(screen.getByText('Price escalation factor')).toBeVisible(); + expect(screen.getByText('1.10701')).toBeVisible(); + expect(screen.getByText('Base year: Award Term 1')).toBeVisible(); + }); + + it('Displays accepted service item without displaying rejection reason markup', async () => { + serviceItem.status = 'APPROVED'; + serviceItem.rejectionReason = null; + render( + , + ); + + expect(screen.getByText('Domestic linehaul')).toBeVisible(); + expect(screen.getByText('$88,553.85')).toBeVisible(); + expect(screen.getByText('Accepted')).toBeVisible(); + + // Expand service item row + await act(async () => { + screen.getByText('Domestic linehaul').click(); + }); + + // Test for rejection reason + expect(screen.queryByText('Rejection Reason')).toBeNull(); + expect(screen.queryByText('Rejection reason test text')).toBeNull(); + + // Test for calculations + expect(screen.getByText('Billable weight (cwt)')).toBeVisible(); + expect(screen.getByText('16.5 cwt')).toBeVisible(); + expect(screen.getByText('Original: 2,000 lbs')).toBeVisible(); + expect(screen.getByText('Estimated: 1,500 lbs')).toBeVisible(); + + expect(screen.getByText('Mileage')).toBeVisible(); + expect(screen.getByText('1,540')).toBeVisible(); + expect(screen.getByText('ZIP 62225 to ZIP 85309')).toBeVisible(); + + expect(screen.getByText('Baseline linehaul price')).toBeVisible(); + expect(screen.getByText('3.148')).toBeVisible(); + expect(screen.getByText('Domestic non-peak')).toBeVisible(); + expect(screen.getByText('Origin service area: 456')).toBeVisible(); + expect(screen.getByText('Requested pickup: 02 Jan 2025')).toBeVisible(); + + expect(screen.getByText('Price escalation factor')).toBeVisible(); + expect(screen.getByText('1.10701')).toBeVisible(); + expect(screen.getByText('Base year: Award Term 1')).toBeVisible(); + }); + + it('Displays rejected basic service item (Move Management) with rejection reason and no calculations', async () => { + render( + , + ); + + expect(screen.getByText('Move Management')).toBeVisible(); + expect(screen.getByText('$88,553.85')).toBeVisible(); + expect(screen.getByText('Rejected')).toBeVisible(); + + // Expand service item row + await act(async () => { + screen.getByText('Move Management').click(); + }); + + // Test for rejection reason + expect(screen.getByText('Rejection Reason')).toBeVisible(); + expect(screen.getByText('Rejection reason test text')).toBeVisible(); + }); +}); diff --git a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx index 603305cd7c2..a9201b3ec3f 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.jsx @@ -62,6 +62,9 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat const isCivilian = grade === 'CIVILIAN_EMPLOYEE'; const renderHaulType = (haulType) => { + if (haulType === '') { + return null; + } return haulType === HAUL_TYPES.LINEHAUL ? 'Linehaul' : 'Shorthaul'; }; // check if the itemName is one of the items recalulated after item edit(updatedItemName). @@ -268,16 +271,18 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat case sectionTypes.incentiveFactors: return (
-
- - - {isFetchingItems && isRecalulatedItem('haulPrice') ? ( - - ) : ( - `$${formatCents(sectionInfo.haulPrice)}` - )} - -
+ {sectionInfo.haulPrice > 0 && ( +
+ + + {isFetchingItems && isRecalulatedItem('haulPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.haulPrice)}` + )} + +
+ )}
@@ -311,6 +316,7 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat )}
+
@@ -321,6 +327,7 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat )}
+
@@ -331,6 +338,36 @@ const getSectionMarkup = (sectionInfo, handleEditOnClick, isFetchingItems, updat )}
+
+ + + {isFetchingItems && isRecalulatedItem('intlPackPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.intlPackPrice)}` + )} + +
+
+ + + {isFetchingItems && isRecalulatedItem('intlUnpackPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.intlUnpackPrice)}` + )} + +
+
+ + + {isFetchingItems && isRecalulatedItem('intlLinehaulPrice') ? ( + + ) : ( + `$${formatCents(sectionInfo.intlLinehaulPrice)}` + )} + +
diff --git a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx index 7fb05d4da7d..4b173db4bdb 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/HeaderSection.test.jsx @@ -219,16 +219,24 @@ const incentivesAdvanceReceivedZeroProps = { setIsSubmitting: jest.fn(), }; +const HAUL_TYPES = { + SHORTHAUL: 'Shorthaul', + LINEHAUL: 'Linehaul', +}; + const incentiveFactorsProps = { sectionInfo: { type: 'incentiveFactors', - haulType: 'Linehaul', + haulType: HAUL_TYPES.LINEHAUL, haulPrice: 6892668, - haulFSC: -143, + haulFSC: 143, packPrice: 20000, unpackPrice: 10000, dop: 15640, ddp: 34640, + intlPackPrice: 1234, + intlUnpackPrice: 12345, + intlLinehaulPrice: 123456, sitReimbursement: 30000, }, }; @@ -469,7 +477,7 @@ describe('PPMHeaderSummary component', () => { expect(screen.getByText('Linehaul Price')).toBeInTheDocument(); expect(screen.getByTestId('haulPrice')).toHaveTextContent('$68,926.68'); expect(screen.getByText('Linehaul Fuel Rate Adjustment')).toBeInTheDocument(); - expect(screen.getByTestId('haulFSC')).toHaveTextContent('-$1.43'); + expect(screen.getByTestId('haulFSC')).toHaveTextContent('$1.43'); expect(screen.getByText('Packing Charge')).toBeInTheDocument(); expect(screen.getByTestId('packPrice')).toHaveTextContent('$200.00'); expect(screen.getByText('Unpacking Charge')).toBeInTheDocument(); @@ -478,6 +486,12 @@ describe('PPMHeaderSummary component', () => { expect(screen.getByTestId('originPrice')).toHaveTextContent('$156.40'); expect(screen.getByText('Destination Price')).toBeInTheDocument(); expect(screen.getByTestId('destinationPrice')).toHaveTextContent('$346.40'); + expect(screen.getByText('International Packing Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlPackPrice')).toHaveTextContent('$12.34'); + expect(screen.getByText('International Unpacking Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlUnpackPrice')).toHaveTextContent('$123.45'); + expect(screen.getByText('International Shipping & Linehaul Charge')).toBeInTheDocument(); + expect(screen.getByTestId('intlLinehaulPrice')).toHaveTextContent('$1,234.56'); expect(screen.getByTestId('sitReimbursement')).toHaveTextContent('$300.00'); }); diff --git a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx index 955389774cd..23d84460bf7 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.jsx @@ -36,6 +36,9 @@ const GCCAndIncentiveInfo = ({ ppmShipmentInfo, updatedItemName, setUpdatedItemN dop: ppmCloseout.dop, ddp: ppmCloseout.ddp, sitReimbursement: ppmCloseout.SITReimbursement, + intlPackPrice: ppmCloseout.intlPackPrice, + intlUnpackPrice: ppmCloseout.intlUnpackPrice, + intlLinehaulPrice: ppmCloseout.intlLinehaulPrice, }; return ( diff --git a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx index 89c94406ccc..6d938da22b2 100644 --- a/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx +++ b/src/components/Office/PPM/PPMHeaderSummary/PPMHeaderSummary.test.jsx @@ -167,7 +167,7 @@ const defaultProps = { describe('PPMHeaderSummary component', () => { describe('displays form', () => { - it('renders blank form on load with defaults', async () => { + it('renders default values', async () => { usePPMShipmentDocsQueries.mockReturnValue(useEditShipmentQueriesReturnValue); useEditShipmentQueries.mockReturnValue(useEditShipmentQueriesReturnValue); renderWithProviders(, mockRoutingConfig); diff --git a/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx b/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx index f275eb0f3a6..0b6412ddfe4 100644 --- a/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx +++ b/src/components/Office/PPM/SitCostBreakdown/SitCostBreakdown.jsx @@ -24,6 +24,11 @@ export default function SitCostBreakdown({ actualWeight, ); + const isEitherAddressOconus = (ppm) => { + return ppm?.destinationAddress?.isOconus || ppm?.pickupAddress?.isOconus; + }; + const isInternationalShipment = isEitherAddressOconus(ppmShipmentInfo); + setEstimatedCost(estimatedCost?.sitCost || 0); return (
@@ -36,18 +41,25 @@ export default function SitCostBreakdown({ SIT Information:
-
- - {estimatedCost.paramsFirstDaySIT.serviceAreaOrigin - ? `Origin service area: ${estimatedCost?.paramsFirstDaySIT.serviceAreaOrigin}` - : `Destination service area: ${estimatedCost?.paramsFirstDaySIT.serviceAreaDestination}`} - -
+ {(estimatedCost.paramsFirstDaySIT.serviceAreaOrigin || + estimatedCost.paramsFirstDaySIT.serviceAreaDestination) && ( +
+ + {estimatedCost.paramsFirstDaySIT.serviceAreaOrigin + ? `Origin service area: ${estimatedCost.paramsFirstDaySIT.serviceAreaOrigin}` + : `Destination service area: ${estimatedCost.paramsFirstDaySIT.serviceAreaDestination}`} + +
+ )}
Actual move date: {formatDate(ppmShipmentInfo.actualMoveDate)}
- {estimatedCost.paramsFirstDaySIT.isPeak ? 'Domestic peak' : 'Domestic non-peak'} + + {isInternationalShipment + ? `${estimatedCost.paramsFirstDaySIT.isPeak ? 'International peak' : 'International non-peak'}` + : `${estimatedCost.paramsFirstDaySIT.isPeak ? 'Domestic peak' : 'Domestic non-peak'}`} +
diff --git a/src/components/Office/RequestedShipments/RequestedShipments.test.jsx b/src/components/Office/RequestedShipments/RequestedShipments.test.jsx index 076edae05ec..ffd550269bf 100644 --- a/src/components/Office/RequestedShipments/RequestedShipments.test.jsx +++ b/src/components/Office/RequestedShipments/RequestedShipments.test.jsx @@ -8,6 +8,7 @@ import { Provider } from 'react-redux'; import { shipments, + shipmentsNoApprovedDate, ntsExternalVendorShipments, ordersInfo, allowancesInfo, @@ -479,6 +480,242 @@ describe('RequestedShipments', () => { expect(counselorRemarks.at(1).textContent).toBe('looks good'); }, ); + + it('calls approveMultipleShipments if move is not available to prime', async () => { + const mockOnSubmit = jest.fn((_, { onSuccess }) => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }).then(async () => { + await onSuccess(); + }); + }); + + const approveMultipleShipments = jest.fn(() => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }); + }); + + const { container } = render( + + + , + ); + + const shipmentInput = container.querySelector('input[name="shipments"]'); + await userEvent.type(shipmentInput, 'ce01a5b8-9b44-4511-8a8d-edb60f2a4aee'); + + const shipmentManagementFeeInput = screen.getByRole('checkbox', { name: 'Move management' }); + await userEvent.click(shipmentManagementFeeInput); + + const counselingFeeInput = screen.getByRole('checkbox', { name: 'Counseling' }); + await userEvent.click(counselingFeeInput); + await userEvent.click(screen.getByRole('button', { name: 'Approve selected' })); + + await userEvent.click(screen.getByText('Approve and send')); + + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockOnSubmit.mock.calls[0]).toEqual([ + { + moveTaskOrderID: moveTaskOrder.id, + ifMatchETag: moveTaskOrder.eTag, + mtoApprovalServiceItemCodes: { + serviceCodeCS: true, + serviceCodeMS: true, + }, + normalize: false, + }, + { + onSuccess: expect.any(Function), + onError: expect.any(Function), + }, + ]); + expect(approveMultipleShipments).toHaveBeenCalled(); + expect(approveMultipleShipments.mock.calls[0]).toEqual([ + { + payload: [ + { + shipmentID: shipments[0].id, + eTag: shipments[0].eTag, + }, + ], + normalize: false, + }, + { + onError: expect.any(Function), + }, + ]); + }); + + it('calls approveMultipleShipments when move is available to prime does NOT have approveDate', async () => { + const mockOnSubmit = jest.fn((_, { onSuccess }) => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }).then(async () => { + await onSuccess(); + }); + }); + + const approveMTOShipment = jest.fn(() => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }); + }); + const approveMultipleShipments = jest.fn(() => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }); + }); + + const { container } = render( + + + , + ); + + const shipmentInput = container.querySelector('input[name="shipments"]'); + await userEvent.type(shipmentInput, 'ce01a5b8-9b44-4511-8a8d-edb60f2a4aee'); + + const shipmentManagementFeeInput = screen.getByRole('checkbox', { name: 'Move management' }); + await userEvent.click(shipmentManagementFeeInput); + + const counselingFeeInput = screen.getByRole('checkbox', { name: 'Counseling' }); + await userEvent.click(counselingFeeInput); + await userEvent.click(screen.getByRole('button', { name: 'Approve selected' })); + + await userEvent.click(screen.getByText('Approve and send')); + + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockOnSubmit.mock.calls[0]).toEqual([ + { + moveTaskOrderID: moveTaskOrder.id, + ifMatchETag: moveTaskOrder.eTag, + mtoApprovalServiceItemCodes: { + serviceCodeCS: true, + serviceCodeMS: false, + }, + normalize: false, + }, + { + onSuccess: expect.any(Function), + onError: expect.any(Function), + }, + ]); + expect(approveMultipleShipments).toHaveBeenCalled(); + expect(approveMultipleShipments.mock.calls[0]).toEqual([ + { + payload: [ + { + shipmentID: shipments[0].id, + eTag: shipments[0].eTag, + }, + ], + normalize: false, + }, + { + onError: expect.any(Function), + }, + ]); + expect(approveMTOShipment).not.toHaveBeenCalled(); + }); + + it('calls approveMTOShipment when move is available to prime and has approveDate', async () => { + const mockOnSubmit = jest.fn((_, { onSuccess }) => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }).then(async () => { + await onSuccess(); + }); + }); + + const approveMTOShipment = jest.fn(() => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }); + }); + const approveMultipleShipments = jest.fn(() => { + return new Promise((resolve) => { + resolve({ response: { status: 200 } }); + }); + }); + + const { container } = render( + + + , + ); + + const shipmentInput = container.querySelector('input[name="shipments"]'); + await userEvent.type(shipmentInput, 'ce01a5b8-9b44-4511-8a8d-edb60f2a4aee'); + + const shipmentManagementFeeInput = screen.getByRole('checkbox', { name: 'Move management' }); + await userEvent.click(shipmentManagementFeeInput); + + const counselingFeeInput = screen.getByRole('checkbox', { name: 'Counseling' }); + await userEvent.click(counselingFeeInput); + await userEvent.click(screen.getByRole('button', { name: 'Approve selected' })); + + await userEvent.click(screen.getByText('Approve and send')); + + expect(mockOnSubmit).toHaveBeenCalled(); + expect(mockOnSubmit.mock.calls[0]).toEqual([ + { + moveTaskOrderID: moveTaskOrder.id, + ifMatchETag: moveTaskOrder.eTag, + mtoApprovalServiceItemCodes: { + serviceCodeCS: true, + serviceCodeMS: false, + }, + normalize: false, + }, + { + onSuccess: expect.any(Function), + onError: expect.any(Function), + }, + ]); + expect(approveMTOShipment).toHaveBeenCalled(); + expect(approveMTOShipment.mock.calls[0]).toEqual([ + { + shipmentID: shipments[0].id, + ifMatchETag: shipments[0].eTag, + operationPath: 'shipment.approveShipmentDiversion', + normalize: false, + }, + { + onError: expect.any(Function), + }, + ]); + expect(approveMultipleShipments).not.toHaveBeenCalled(); + }); }); describe('External vendor shipments', () => { diff --git a/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js b/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js index c9eae8b2667..d3cb9c2a1e8 100644 --- a/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js +++ b/src/components/Office/RequestedShipments/RequestedShipmentsTestData.js @@ -172,6 +172,67 @@ export const shipments = [ }, ]; +export const shipmentsNoApprovedDate = [ + { + createdAt: '2020-06-10T15:58:02.404029Z', + customerRemarks: 'please treat gently', + counselorRemarks: 'looks good', + destinationAddress: { + city: 'Fairfield', + country: 'US', + eTag: 'MjAyMC0wNi0xMFQxNTo1ODowMi4zODk0MTJa', + id: '672ff379-f6e3-48b4-a87d-796713f8f997', + postalCode: '94535', + state: 'CA', + streetAddress1: '987 Any Avenue', + streetAddress2: 'P.O. Box 9876', + streetAddress3: 'c/o Some Person', + }, + eTag: 'MjAyMC0wNi0xMFQxNTo1ODowMi40MDQwMzFa', + id: 'ce01a5b8-9b44-4511-8a8d-edb60f2a4aea', + moveTaskOrderID: '9c7b255c-2981-4bf8-839f-61c7458e2b4d', + pickupAddress: { + city: 'Beverly Hills', + country: 'US', + eTag: 'MjAyMC0wNi0xMFQxNTo1ODowMi4zODQ3Njla', + id: '1686751b-ab36-43cf-b3c9-c0f467d13c19', + postalCode: '90210', + state: 'CA', + streetAddress1: '123 Any Street', + streetAddress2: 'P.O. Box 12345', + streetAddress3: 'c/o Some Person', + }, + rejectionReason: 'shipment not good enough', + requestedPickupDate: '2018-03-15', + scheduledPickupDate: '2018-03-16', + secondaryDeliveryAddress: { + city: 'Beverly Hills', + country: 'US', + eTag: 'MjAyMC0wNi0xMFQxNTo1ODowMi4zOTkzMlo=', + id: '15e8f6cc-e1d7-44b2-b1e0-fcb3d6442831', + postalCode: '90210', + state: 'CA', + streetAddress1: '123 Any Street', + streetAddress2: 'P.O. Box 12345', + streetAddress3: 'c/o Some Person', + }, + secondaryPickupAddress: { + city: 'Beverly Hills', + country: 'US', + eTag: 'MjAyMC0wNi0xMFQxNTo1ODowMi4zOTM4OTZa', + id: '9b79e0c3-8ed5-4fb8-aa36-95845707d8ee', + postalCode: '90210', + state: 'CA', + streetAddress1: '123 Any Street', + streetAddress2: 'P.O. Box 12345', + streetAddress3: 'c/o Some Person', + }, + shipmentType: SHIPMENT_OPTIONS.HHG, + status: 'SUBMITTED', + updatedAt: '2020-06-10T15:58:02.404031Z', + }, +]; + export const ntsExternalVendorShipments = [ { approvedDate: '0001-01-01', diff --git a/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx b/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx index 69793103aae..a296057ec95 100644 --- a/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx +++ b/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx @@ -52,6 +52,7 @@ const SubmittedRequestedShipments = ({ customerInfo, approveMTO, approveMTOShipment, + approveMultipleShipments, handleAfterSuccess, missingRequiredOrdersInfo, errorIfMissing, @@ -149,6 +150,15 @@ const SubmittedRequestedShipments = ({ ); }; + const getUpdateMultipleShipmentPayload = (shipments) => { + return shipments.map((shipment) => { + return { + shipmentID: shipment.id, + eTag: shipment.eTag, + }; + }); + }; + const queryClient = useQueryClient(); const shipmentMutation = useMutation(updateMTOShipment, { onSuccess: (updatedMTOShipments) => { @@ -200,7 +210,9 @@ const SubmittedRequestedShipments = ({ }); try { - await Promise.all(ppmShipmentPromise).then(() => { + await Promise.all(ppmShipmentPromise); + + await new Promise((resolve, reject) => { approveMTO( { moveTaskOrderID: moveTaskOrder.id, @@ -211,38 +223,71 @@ const SubmittedRequestedShipments = ({ { onSuccess: async () => { try { - await Promise.all( - filteredShipments.map((shipment) => { - let operationPath = 'shipment.approveShipment'; - - if (shipment.approvedDate && moveTaskOrder.availableToPrimeAt) { - operationPath = 'shipment.approveShipmentDiversion'; - } - return approveMTOShipment( - { - shipmentID: shipment.id, - operationPath, - ifMatchETag: shipment.eTag, - normalize: false, + // if the move is not available to prime yet, we use the new approveShipments api + // to approve multiple shipments in one call and make it available to prime at the end. + // else we use the old looping method to account for approveShipmentDiversion api call. + if (!moveTaskOrder.availableToPrimeAt && filteredShipments.length) { + await approveMultipleShipments( + { + payload: getUpdateMultipleShipmentPayload(filteredShipments), + normalize: false, + }, + { + onError: () => { + setSubmitting(false); + setFlashMessage(null); }, - { - onError: () => { - setSubmitting(false); - setFlashMessage(null); + }, + ); + } else { + // Approve each shipment asynchronously + await Promise.all( + filteredShipments.map((shipment) => { + if (shipment.approvedDate) { + return approveMTOShipment( + { + shipmentID: shipment.id, + operationPath: 'shipment.approveShipmentDiversion', + ifMatchETag: shipment.eTag, + normalize: false, + }, + { + onError: () => { + setSubmitting(false); + setFlashMessage(null); + reject(); + }, + }, + ); + } + return approveMultipleShipments( + { + payload: getUpdateMultipleShipmentPayload([shipment]), + normalize: false, }, - }, - ); - }), - ).then(() => { - setFlashMessage('TASK_ORDER_CREATE_SUCCESS', 'success', 'Task order created successfully.'); - handleAfterSuccess('../mto', { showMTOpostedMessage: true }); - }); + { + onError: () => { + setSubmitting(false); + setFlashMessage(null); + reject(); + }, + }, + ); + }), + ); + } + // All shipments approved, set flash message and navigate + setFlashMessage('TASK_ORDER_CREATE_SUCCESS', 'success', 'Task order created successfully.'); + handleAfterSuccess('../mto', { showMTOpostedMessage: true }); + resolve(); } catch { setSubmitting(false); + reject(); } }, onError: () => { setSubmitting(false); + reject(); }, }, ); @@ -311,6 +356,7 @@ const SubmittedRequestedShipments = ({ onSubmit={debouncedSubmit} counselingFee={formik.values.counselingFee} shipmentManagementFee={formik.values.shipmentManagementFee} + isSubmitting={formik.isSubmitting} />
diff --git a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.jsx b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.jsx index 814e82d124a..fea91773c2e 100644 --- a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.jsx +++ b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.jsx @@ -14,7 +14,6 @@ import { } from 'constants/serviceItems'; const times = ; -const equals = ; const ServiceItemCalculations = ({ itemCode, @@ -37,10 +36,6 @@ const ServiceItemCalculations = ({ return times; } - if (index === length - 1) { - return equals; - } - return null; }; diff --git a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.module.scss b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.module.scss index c83658fa704..48e25cd1efb 100644 --- a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.module.scss +++ b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.module.scss @@ -59,8 +59,6 @@ .col:last-of-type { @include u-margin-right(0); - @include u-text-align('right'); - hr { width: 100%; } diff --git a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.test.jsx b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.test.jsx index 79a3704f074..7d7f1bf62c5 100644 --- a/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.test.jsx +++ b/src/components/Office/ServiceItemCalculations/ServiceItemCalculations.test.jsx @@ -77,10 +77,8 @@ describe('ServiceItemCalculations DLH', () => { it('renders icons', () => { const wrapper = serviceItemCalculationsLarge; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(3); - expect(equalsIcons.length).toBe(1); }); }); @@ -96,10 +94,8 @@ describe('ServiceItemCalculations DLH', () => { it('renders no icons', () => { const wrapper = serviceItemCalculationsSmall; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(0); - expect(equalsIcons.length).toBe(0); }); }); @@ -162,10 +158,8 @@ describe('ServiceItemCalculations DCRT', () => { it('renders icons', () => { const wrapper = serviceItemCalculationsLarge; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(2); - expect(equalsIcons.length).toBe(1); }); }); @@ -181,10 +175,8 @@ describe('ServiceItemCalculations DCRT', () => { it('renders no icons', () => { const wrapper = serviceItemCalculationsSmall; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(0); - expect(equalsIcons.length).toBe(0); }); }); @@ -248,10 +240,8 @@ describe('ServiceItemCalculations DUCRT', () => { it('renders icons', () => { const wrapper = serviceItemCalculationsLarge; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(2); - expect(equalsIcons.length).toBe(1); }); }); @@ -267,10 +257,8 @@ describe('ServiceItemCalculations DUCRT', () => { it('renders no icons', () => { const wrapper = serviceItemCalculationsSmall; const timesIcons = wrapper.find('[icon="times"]'); - const equalsIcons = wrapper.find('[icon="equals"]'); expect(timesIcons.length).toBe(0); - expect(equalsIcons.length).toBe(0); }); }); diff --git a/src/components/Office/ServiceItemCalculations/helpers.js b/src/components/Office/ServiceItemCalculations/helpers.js index c5d26d73e75..8412e4192d7 100644 --- a/src/components/Office/ServiceItemCalculations/helpers.js +++ b/src/components/Office/ServiceItemCalculations/helpers.js @@ -687,7 +687,7 @@ const uncappedRequestTotal = (params) => { const totalAmountRequested = (totalAmount) => { const value = toDollarString(formatCents(totalAmount)); - const label = `${SERVICE_ITEM_CALCULATION_LABELS.Total}:`; + const label = `${SERVICE_ITEM_CALCULATION_LABELS.Total}: `; const detail = ''; return calculation(value, label, formatDetail(detail)); diff --git a/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx b/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx index 689a790ca64..76be9ca5d57 100644 --- a/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx +++ b/src/components/Office/ServiceItemDetails/ServiceItemDetails.jsx @@ -491,8 +491,11 @@ const ServiceItemDetails = ({ id, code, details, serviceRequestDocs, shipment, s case 'ISLH': case 'IHPK': case 'IHUPK': + case 'IUBPK': + case 'IUBUPK': case 'POEFSC': - case 'PODFSC': { + case 'PODFSC': + case 'UBP': { detailSection = (
diff --git a/src/components/Office/ServiceItemDetails/ServiceItemDetails.test.jsx b/src/components/Office/ServiceItemDetails/ServiceItemDetails.test.jsx index d9267d7e572..fab0ded5351 100644 --- a/src/components/Office/ServiceItemDetails/ServiceItemDetails.test.jsx +++ b/src/components/Office/ServiceItemDetails/ServiceItemDetails.test.jsx @@ -523,44 +523,70 @@ describe('ServiceItemDetails Crating Rejected', () => { }); }); -describe('ServiceItemDetails Estimated Price for DLH, DSH, FSC, DOP, DDP, DPK, DUPK', () => { - it.each([['DLH'], ['DSH'], ['FSC'], ['DOP'], ['DDP'], ['DPK'], ['DUPK']])( - 'renders the formatted estimated price field for the service items', - (code) => { - render( - , - ); - - expect(screen.getByText('Estimated Price:')).toBeInTheDocument(); - expect(screen.getByText('$28.00')).toBeInTheDocument(); - }, - ); +describe('ServiceItemDetails Estimated Price for DLH, DSH, FSC, DOP, DDP, DPK, DUPK, ISLH, IHPK, IHUPK, IUBPK, IUBUPK, POEFSC, PODFSC, UBP', () => { + it.each([ + ['DLH'], + ['DSH'], + ['FSC'], + ['DOP'], + ['DDP'], + ['DPK'], + ['DUPK'], + ['ISLH'], + ['IHPK'], + ['IHUPK'], + ['IUBPK'], + ['IUBUPK'], + ['POEFSC'], + ['PODFSC'], + ['UBP'], + ])('renders the formatted estimated price field for the service item: %s', (code) => { + render( + , + ); + + expect(screen.getByText('Estimated Price:')).toBeInTheDocument(); + expect(screen.getByText('$28.00')).toBeInTheDocument(); + }); const noEstimatePriceDetails = {}; - it.each([['DLH'], ['DSH'], ['FSC'], ['DOP'], ['DDP'], ['DPK'], ['DUPK']])( - 'renders - for estimated price when price is not in details', - (code) => { - render( - , - ); - - expect(screen.getByText('Estimated Price:')).toBeInTheDocument(); - expect(screen.getByText('-')).toBeInTheDocument(); - }, - ); + it.each([ + ['DLH'], + ['DSH'], + ['FSC'], + ['DOP'], + ['DDP'], + ['DPK'], + ['DUPK'], + ['ISLH'], + ['IHPK'], + ['IHUPK'], + ['IUBPK'], + ['IUBUPK'], + ['POEFSC'], + ['PODFSC'], + ['UBP'], + ])('renders - for estimated price when price is not in details for the service item: %s', (code) => { + render( + , + ); + + expect(screen.getByText('Estimated Price:')).toBeInTheDocument(); + expect(screen.getByText('-')).toBeInTheDocument(); + }); }); describe('ServiceItemDetails Price for MS, CS', () => { diff --git a/src/components/Office/ShipmentApprovalPreview/ShipmentApprovalPreview.jsx b/src/components/Office/ShipmentApprovalPreview/ShipmentApprovalPreview.jsx index 200feb4991a..5aa4288ac9d 100644 --- a/src/components/Office/ShipmentApprovalPreview/ShipmentApprovalPreview.jsx +++ b/src/components/Office/ShipmentApprovalPreview/ShipmentApprovalPreview.jsx @@ -26,6 +26,7 @@ const ShipmentApprovalPreview = ({ onSubmit, counselingFee, shipmentManagementFee, + isSubmitting, }) => { return (
@@ -47,7 +48,7 @@ const ShipmentApprovalPreview = ({

Preview and post move task order

Is all the information shown correct and ready to send to Global Relocation Services?

-
)} + {!shipment.ppmShipment && ( +
+
Origin Rate Area:
+
{shipment.originRateArea?.rateAreaName ? shipment.originRateArea.rateAreaName : '—'}
+
{shipment.originRateArea?.rateAreaId ? shipment.originRateArea.rateAreaId : '—'}
+
+ )}
Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.pickupAddress)}
@@ -218,6 +225,13 @@ const Shipment = ({ shipment, moveId, onDelete, mtoServiceItems }) => { )}
+ {!shipment.ppmShipment && ( +
+
Destination Rate Area:
+
{shipment.destinationRateArea?.rateAreaName ? shipment.destinationRateArea.rateAreaName : '—'}
+
{shipment.destinationRateArea?.rateAreaId ? shipment.destinationRateArea.rateAreaId : '—'}
+
+ )}
Delivery Address:
{formatPrimeAPIShipmentAddress(shipment.destinationAddress)}
@@ -322,6 +336,17 @@ const Shipment = ({ shipment, moveId, onDelete, mtoServiceItems }) => {
PPM Approved at:
{formatDateFromIso(shipment.ppmShipment.approvedAt, 'YYYY-MM-DD')}
+
+
Origin Rate Area:
+
+ {shipment.ppmShipment?.originRateArea?.rateAreaName + ? shipment.ppmShipment?.originRateArea.rateAreaName + : '—'} +
+
+ {shipment.ppmShipment?.originRateArea?.rateAreaId ? shipment.ppmShipment?.originRateArea.rateAreaId : '—'} +
+
Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.ppmShipment.pickupAddress)}
@@ -334,6 +359,19 @@ const Shipment = ({ shipment, moveId, onDelete, mtoServiceItems }) => {
Third Pickup Address:
{formatPrimeAPIShipmentAddress(shipment.ppmShipment.tertiaryPickupAddress)}
+
+
Destination Rate Area:
+
+ {shipment.ppmShipment?.destinationRateArea?.rateAreaName + ? shipment.ppmShipment?.destinationRateArea.rateAreaName + : '—'} +
+
+ {shipment.ppmShipment?.destinationRateArea?.rateAreaId + ? shipment.ppmShipment?.destinationRateArea.rateAreaId + : '—'} +
+
Delivery Address:
{formatPrimeAPIShipmentAddress(shipment.ppmShipment.destinationAddress)}
diff --git a/src/components/PrimeUI/Shipment/Shipment.test.jsx b/src/components/PrimeUI/Shipment/Shipment.test.jsx index 7bfca7243ed..c953cc3d3e2 100644 --- a/src/components/PrimeUI/Shipment/Shipment.test.jsx +++ b/src/components/PrimeUI/Shipment/Shipment.test.jsx @@ -41,6 +41,11 @@ const approvedMoveTaskOrder = { streetAddress2: 'P.O. Box 9876', streetAddress3: 'c/o Some Person', }, + destinationRateArea: { + id: 'bfe61147-5fd7-426e-b473-54ccf77bde37', + rateAreaName: 'California-North', + rateAreaId: 'US87', + }, eTag: 'MjAyMS0xMC0xOFQxODoyNDo0MS4zNzc5Nzha', firstAvailableDeliveryDate: null, id: 'ce01a5b8-9b44-4511-8a8d-edb60f2a4aee', @@ -54,6 +59,11 @@ const approvedMoveTaskOrder = { streetAddress2: 'P.O. Box 12345', streetAddress3: 'c/o Some Person', }, + originRateArea: { + id: 'bfe61147-5fd7-426e-b473-54ccf77bde36', + rateAreaName: 'California-South', + rateAreaId: 'US88', + }, primeActualWeight: 2000, primeEstimatedWeight: 1400, primeEstimatedWeightRecordedDate: null, @@ -209,6 +219,11 @@ describe('Shipment details component', () => { expect(field).toBeInTheDocument(); expect(field.nextElementSibling.textContent).toBe(shipment.actualSpouseProGearWeight.toString()); + field = screen.getByText('Origin Rate Area:'); + expect(field).toBeInTheDocument(); + expect(field.nextElementSibling.textContent).toContain(shipment.originRateArea.rateAreaName); + expect(field.nextElementSibling.nextElementSibling.textContent).toContain(shipment.originRateArea.rateAreaId); + field = screen.getByText('Pickup Address:'); expect(field).toBeInTheDocument(); expect(field.nextElementSibling.textContent).toContain(shipment.pickupAddress.city); @@ -217,6 +232,11 @@ describe('Shipment details component', () => { expect(field.nextElementSibling.textContent).toContain(shipment.pickupAddress.streetAddress2); expect(field.nextElementSibling.textContent).toContain(shipment.pickupAddress.postalCode); + field = screen.getByText('Destination Rate Area:'); + expect(field).toBeInTheDocument(); + expect(field.nextElementSibling.textContent).toContain(shipment.destinationRateArea.rateAreaName); + expect(field.nextElementSibling.nextElementSibling.textContent).toContain(shipment.destinationRateArea.rateAreaId); + field = screen.getByText('Delivery Address:'); expect(field).toBeInTheDocument(); expect(field.nextElementSibling.textContent).toContain(shipment.destinationAddress.city); @@ -412,6 +432,11 @@ const ppmShipment = { status: 'SUBMITTED', submittedAt: '2022-07-01T13:41:33.252Z', updatedAt: '2022-07-01T14:23:19.780Z', + originRateArea: { + id: 'bfe61147-5fd7-426e-b473-54ccf77bde38', + rateAreaName: 'Kentucky', + rateAreaId: 'US28', + }, pickupAddress: { streetAddress1: '111 Test Street', streetAddress2: '222 Test Street', @@ -428,6 +453,11 @@ const ppmShipment = { state: 'KY', postalCode: '42702', }, + destinationRateArea: { + id: 'bfe61147-5fd7-426e-b473-54ccf77bde38', + rateAreaName: 'Kentucky', + rateAreaId: 'US28', + }, destinationAddress: { streetAddress1: '222 Test Street', streetAddress2: '333 Test Street', @@ -550,6 +580,28 @@ describe('PPM shipments are handled', () => { expect(field).not.toBeInTheDocument(); }); + it('PPM displays rate areas', async () => { + render( + + + , + ); + + let field = screen.getByText('Origin Rate Area:'); + expect(field).toBeInTheDocument(); + expect(field.nextElementSibling.textContent).toContain(ppmShipment.ppmShipment.originRateArea.rateAreaName); + expect(field.nextElementSibling.nextElementSibling.textContent).toContain( + ppmShipment.ppmShipment.originRateArea.rateAreaId, + ); + + field = screen.getByText('Destination Rate Area:'); + expect(field).toBeInTheDocument(); + expect(field.nextElementSibling.textContent).toContain(ppmShipment.ppmShipment.destinationRateArea.rateAreaName); + expect(field.nextElementSibling.nextElementSibling.textContent).toContain( + ppmShipment.ppmShipment.destinationRateArea.rateAreaId, + ); + }); + it('PPM can be deleted', async () => { const onDelete = jest.fn(); diff --git a/src/components/Table/TableQueue.jsx b/src/components/Table/TableQueue.jsx index d657df5007f..32e13cfc75f 100644 --- a/src/components/Table/TableQueue.jsx +++ b/src/components/Table/TableQueue.jsx @@ -125,6 +125,7 @@ const TableQueue = ({ currentPage, currentPageSize, viewAsGBLOC: selectedGbloc, + activeRole, }); // react-table setup below diff --git a/src/constants/MoveHistory/Database/BooleanFields.js b/src/constants/MoveHistory/Database/BooleanFields.js index 32a4cc5f7c7..c14cb67f56c 100644 --- a/src/constants/MoveHistory/Database/BooleanFields.js +++ b/src/constants/MoveHistory/Database/BooleanFields.js @@ -12,6 +12,7 @@ export default { missing_receipt: 'missing_receipt', organizational_clothing_and_individual_equipment: 'organizational_clothing_and_individual_equipment', gun_safe: 'gun_safe', + admin_restricted_weight_location: 'admin_restricted_weight_location', email_is_preferred: 'email_is_preferred', phone_is_preferred: 'phone_is_preferred', uses_external_vendor: 'uses_external_vendor', diff --git a/src/constants/MoveHistory/Database/FieldMappings.js b/src/constants/MoveHistory/Database/FieldMappings.js index 6ba6b0793d4..53724afcd8a 100644 --- a/src/constants/MoveHistory/Database/FieldMappings.js +++ b/src/constants/MoveHistory/Database/FieldMappings.js @@ -39,6 +39,8 @@ export default { required_medical_equipment_weight: 'Required medical equipment', organizational_clothing_and_individual_equipment: 'OCIE', gun_safe: 'Gun Safe', + admin_restricted_weight_location: 'Admin restricted weight location', + weight_restriction: 'Weight restriction', requested_pickup_date: 'Requested pickup date', grade: 'Pay grade', shipment_type: 'Shipment type', @@ -148,4 +150,13 @@ export default { approvals_requested_at: 'Approvals requested at', approved_at: 'Approved at', counseling_office_name: 'Counseling office', + assigned_sc: 'Counselor assigned', + assigned_sc_ppm: 'Closeout counselor assigned', + assigned_too: 'Task ordering officer assigned', + assigned_tio: 'Task invoicing officer assigned', + re_assigned_sc: 'Counselor reassigned', + re_assigned_sc_ppm: 'Closeout counselor reassigned', + re_assigned_too: 'Task ordering officer reassigned', + re_assigned_tio: 'Task invoicing officer reassigned', + available_to_prime_at: 'Available to Prime at', }; diff --git a/src/constants/MoveHistory/Database/Tables.js b/src/constants/MoveHistory/Database/Tables.js index f860416dda7..361c40f11c9 100644 --- a/src/constants/MoveHistory/Database/Tables.js +++ b/src/constants/MoveHistory/Database/Tables.js @@ -20,4 +20,5 @@ export default { progear_weight_tickets: 'progear_weight_tickets', gsr_appeals: 'gsr_appeals', shipment_address_updates: 'shipment_address_updates', + payment_service_items: 'payment_service_items', }; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.jsx new file mode 100644 index 00000000000..aa923de3d28 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import { shipmentTypes as s } from 'constants/shipments'; + +export default { + action: a.UPDATE, + eventName: o.approveShipments, + tableName: t.mto_shipments, + getEventNameDisplay: () => 'Approved shipment', + getDetails: ({ context }) => ( + <> + {s[context[0]?.shipment_type]} shipment #{context[0]?.shipment_locator.toUpperCase()} + + ), +}; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.test.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.test.jsx new file mode 100644 index 00000000000..b5090385057 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments.test.jsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; + +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import e from 'constants/MoveHistory/EventTemplates/ApproveShipments/approveShipments'; + +describe('when given an Approved shipment history record', () => { + const historyRecord = { + action: 'UPDATE', + changedValues: { status: 'APPROVED' }, + eventName: 'approveShipments', + oldValues: { shipment_type: 'HHG' }, + tableName: 'mto_shipments', + context: [ + { + shipment_id_abbr: '2fa5c', + shipment_type: 'HHG', + shipment_locator: 'ABC123-01', + }, + ], + }; + it('correctly matches to the Approved shipment template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper value in the details field', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('HHG shipment #ABC123-01')).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.jsx new file mode 100644 index 00000000000..ed36eae8bda --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.jsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; + +export default { + action: a.UPDATE, + eventName: o.approveShipments, + tableName: t.moves, + getEventNameDisplay: () => 'Approved shipment', + getDetails: (historyRecord) => , +}; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.test.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.test.jsx new file mode 100644 index 00000000000..f477051bf49 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove.test.jsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react'; + +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import e from 'constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsApproveMove'; + +describe('when given an Approved shipment, Approved move history record', () => { + const historyRecord = { + action: 'UPDATE', + changedValues: { status: 'APPROVED' }, + eventName: 'approveShipments', + oldValues: { status: 'APPROVALS REQUESTED' }, + tableName: 'moves', + }; + it('correctly matches to the Approved shipment, Approved move template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper value in the details field', () => { + const template = getTemplate(historyRecord); + render(template.getDetails(historyRecord)); + expect(screen.getByText('Status')).toBeInTheDocument(); + expect(screen.getByText(': APPROVED')).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.jsx new file mode 100644 index 00000000000..ce2f87dcd9e --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.jsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import { shipmentTypes as SHIPMENT_TYPE } from 'constants/shipments'; +import { getMtoShipmentLabel } from 'utils/formatMtoShipment'; + +export default { + action: a.INSERT, + eventName: o.approveShipments, + tableName: t.mto_service_items, + getEventNameDisplay: () => 'Approved service item', + getDetails: (historyRecord) => { + const shipmentLabel = getMtoShipmentLabel(historyRecord); + return ( + <> + {SHIPMENT_TYPE[shipmentLabel.shipment_type]} shipment #{shipmentLabel.shipment_locator} + {', '} + {shipmentLabel.service_item_name} + + ); + }, +}; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.test.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.test.jsx new file mode 100644 index 00000000000..a50d279bf9f --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsServiceItem.test.jsx @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react'; + +import getTemplate from 'constants/MoveHistory/TemplateManager'; + +describe('when given a Create standard service item history record', () => { + const historyRecord = { + action: 'INSERT', + context: [ + { + shipment_type: 'HHG', + shipment_id_abbr: 'a1b2c', + shipment_locator: 'ABC123-01', + name: 'Domestic linehaul', + }, + ], + eventName: 'approveShipments', + tableName: 'mto_service_items', + }; + it('correctly matches the Create standard service item event', () => { + const template = getTemplate(historyRecord); + render(template.getDetails(historyRecord)); + expect(template.getEventNameDisplay(template)).toEqual('Approved service item'); + expect(screen.getByText('HHG shipment #ABC123-01', { exact: false })).toBeInTheDocument(); + expect(screen.getByText('Domestic linehaul', { exact: false })).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.jsx new file mode 100644 index 00000000000..a1c2e1a9840 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { formatMoveHistoryMaxBillableWeight } from 'utils/formatters'; + +export default { + action: a.UPDATE, + eventName: o.approveShipments, + tableName: t.entitlements, + getEventNameDisplay: () => 'Updated shipment', + getDetails: (historyRecord) => , +}; diff --git a/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.test.jsx b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.test.jsx new file mode 100644 index 00000000000..9447321e109 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances.test.jsx @@ -0,0 +1,24 @@ +import { render, screen } from '@testing-library/react'; + +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import e from 'constants/MoveHistory/EventTemplates/ApproveShipments/approveShipmentsUpdateAllowances'; + +describe('when given an Approved shipment, Updated allowances history record', () => { + const historyRecord = { + action: 'UPDATE', + changedValues: { authorized_weight: 13230 }, + eventName: 'approveShipments', + tableName: 'entitlements', + }; + it('correctly matches to the Approved shipment, Updated allowances template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper value in the details field', () => { + const template = getTemplate(historyRecord); + render(template.getDetails(historyRecord)); + expect(screen.getByText('Max billable weight')).toBeInTheDocument(); + expect(screen.getByText(': 13,230 lbs')).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.jsx b/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.jsx new file mode 100644 index 00000000000..1764fc61497 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; + +export default { + action: a.UPDATE, + eventName: o.finishDocumentReview, + tableName: t.moves, + getEventNameDisplay: () => 'Updated move', + getDetails: ({ changedValues }) => ( + <> +
PPM Closeout Complete
+ {changedValues?.sc_assigned_id !== undefined ?
Closeout Counselor Unassigned
: null} + + ), +}; diff --git a/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.test.jsx b/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.test.jsx new file mode 100644 index 00000000000..967f33d05e2 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves.test.jsx @@ -0,0 +1,43 @@ +import { screen, render } from '@testing-library/react'; + +import e from 'constants/MoveHistory/EventTemplates/FinishDocumentReview/FinishDocumentReviewMoves'; +import getTemplate from 'constants/MoveHistory/TemplateManager'; + +describe('When given a completed services counseling for a move', () => { + const historyRecord = { + action: 'UPDATE', + eventName: 'finishDocumentReview', + tableName: 'moves', + }; + it('correctly matches the update mto status services counseling completed event to the proper template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper name in the event name display column', () => { + const template = getTemplate(historyRecord); + + render(template.getEventNameDisplay(historyRecord)); + expect(screen.getByText('Updated move')).toBeInTheDocument(); + }); + + it('displays default when SC ID is not present', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('PPM Closeout Complete')).toBeInTheDocument(); + expect(screen.queryByText('Closeout Counselor Unassigned')).not.toBeInTheDocument(); + }); + + it('displays correct details when a SC is unassigned', () => { + historyRecord.changedValues = { + ...historyRecord.changedValues, + sc_assigned_id: null, + }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('PPM Closeout Complete')).toBeInTheDocument(); + expect(screen.getByText('Closeout Counselor Unassigned')).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx new file mode 100644 index 00000000000..000e6ec56f7 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.jsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import { MOVE_STATUSES } from 'shared/constants'; + +export default { + action: a.UPDATE, + eventName: o.deleteAssignedOfficeUser, + tableName: t.moves, + getEventNameDisplay: () => 'Updated move', + getDetails: ({ changedValues, oldValues }) => { + if (changedValues.sc_assigned_id === null && oldValues?.status === MOVE_STATUSES.NEEDS_SERVICE_COUNSELING) + return <>Counselor unassigned; + if (changedValues.sc_assigned_id === null && oldValues?.status !== MOVE_STATUSES.NEEDS_SERVICE_COUNSELING) + return <>Closeout counselor unassigned; + if (changedValues.too_assigned_id === null) return <>Task ordering officer unassigned; + if (changedValues.tio_assigned_id === null) return <>Task invoicing officer unassigned; + return <>Unassigned; + }, +}; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx new file mode 100644 index 00000000000..5c9a613271b --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser.test.jsx @@ -0,0 +1,58 @@ +import { screen, render } from '@testing-library/react'; + +import e from 'constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/DeleteAssignedOfficeUser'; +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import { MOVE_STATUSES } from 'shared/constants'; + +describe('When given a move that has been unassigned', () => { + const historyRecord = { + action: 'UPDATE', + eventName: 'deleteAssignedOfficeUser', + tableName: 'moves', + changedValues: { + sc_assigned_id: null, + }, + }; + + it('correctly matches the template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper name in the event name display column', () => { + const template = getTemplate(historyRecord); + + render(template.getEventNameDisplay(historyRecord)); + expect(screen.getByText('Updated move')).toBeInTheDocument(); + }); + + describe('displays the proper details for', () => { + it('closeout counselor', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor unassigned')).toBeInTheDocument(); + }); + it('services counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Counselor unassigned')).toBeInTheDocument(); + }); + it('task ordering officer', () => { + historyRecord.changedValues = { too_assigned_id: null }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task ordering officer unassigned')).toBeInTheDocument(); + }); + it('task invoicing officer', () => { + historyRecord.changedValues = { tio_assigned_id: null }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task invoicing officer unassigned')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.jsx new file mode 100644 index 00000000000..16e43bc64b6 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.jsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { formatAssignedOfficeUserFromContext } from 'utils/formatters'; + +const formatChangedValues = (historyRecord) => { + const newChangedValues = { + ...formatAssignedOfficeUserFromContext(historyRecord), + }; + + return { ...historyRecord, changedValues: newChangedValues }; +}; + +export default { + action: a.UPDATE, + eventName: o.updateAssignedOfficeUser, + tableName: t.moves, + getEventNameDisplay: () => 'Updated move', + getDetails: (historyRecord) => , +}; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx new file mode 100644 index 00000000000..f1223b4b3a5 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser.test.jsx @@ -0,0 +1,124 @@ +import { screen, render } from '@testing-library/react'; + +import e from 'constants/MoveHistory/EventTemplates/UpdateAssignedOfficeUser/UpdateAssignedOfficeUser'; +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import { MOVE_STATUSES } from 'shared/constants'; + +describe('When given a move that has been assigned', () => { + const historyRecord = { + action: 'UPDATE', + eventName: 'updateAssignedOfficeUser', + tableName: 'moves', + changedValues: { + sc_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137', + }, + oldValues: { + sc_assigned_id: null, + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, + }, + context: [{ assigned_office_user_last_name: 'Daniels', assigned_office_user_first_name: 'Jayden' }], + }; + + it('correctly matches the template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper name in the event name display column', () => { + const template = getTemplate(historyRecord); + + render(template.getEventNameDisplay(historyRecord)); + expect(screen.getByText('Updated move')).toBeInTheDocument(); + }); + + describe('displays the proper details for', () => { + it('assignment of a services counselor', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Counselor assigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('reassignment of a services counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.NEEDS_SERVICE_COUNSELING, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Counselor reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('assignment of a closeout counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: null, + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor assigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('reassignment of a closeout counselor', () => { + const template = getTemplate(historyRecord); + historyRecord.oldValues = { + sc_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64', + status: MOVE_STATUSES.SERVICE_COUNSELING_COMPLETED, + }; + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Closeout counselor reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Daniels, Jayden')).toBeInTheDocument(); + }); + it('assignment of a task ordering officer', () => { + historyRecord.changedValues = { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { too_assigned_id: null }; + historyRecord.context = [ + { assigned_office_user_last_name: 'Robinson', assigned_office_user_first_name: 'Brian' }, + ]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task ordering officer assigned')).toBeInTheDocument(); + expect(screen.getByText(': Robinson, Brian')).toBeInTheDocument(); + }); + it('reassignment of a task ordering officer', () => { + historyRecord.changedValues = { too_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { too_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64' }; + historyRecord.context = [ + { assigned_office_user_last_name: 'Robinson', assigned_office_user_first_name: 'Brian' }, + ]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task ordering officer reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Robinson, Brian')).toBeInTheDocument(); + }); + it('assignment of a task invoicing officer', () => { + historyRecord.changedValues = { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { tio_assigned_id: null }; + historyRecord.context = [{ assigned_office_user_last_name: 'Luvu', assigned_office_user_first_name: 'Frankie' }]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task invoicing officer assigned')).toBeInTheDocument(); + expect(screen.getByText(': Luvu, Frankie')).toBeInTheDocument(); + }); + it('reassignment of a task invoicing officer', () => { + historyRecord.changedValues = { tio_assigned_id: 'fb625e3c-067c-49d7-8fd9-88ef040e6137' }; + historyRecord.oldValues = { tio_assigned_id: '759a87ad-dc75-4b34-b551-d31309a79f64' }; + historyRecord.context = [{ assigned_office_user_last_name: 'Luvu', assigned_office_user_first_name: 'Frankie' }]; + + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Task invoicing officer reassigned')).toBeInTheDocument(); + expect(screen.getByText(': Luvu, Frankie')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.jsx index 6fda8260ba0..033dbada873 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.jsx @@ -9,5 +9,12 @@ export default { eventName: o.updateMTOStatusServiceCounselingCompleted, tableName: t.moves, getEventNameDisplay: () => 'Updated move', - getDetails: () => <> Counseling Completed , + getDetails: ({ changedValues }) => { + return ( + <> +
Counseling Completed
+ {changedValues?.sc_assigned_id !== undefined ?
Counselor Unassigned
: null} + + ); + }, }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.test.jsx index 153216daf83..d4422ca4cc0 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMTOStatusServiceCounselingCompleted/updateMTOStatusServiceCounselingCompleted.test.jsx @@ -26,4 +26,15 @@ describe('When given a completed services counseling for a move', () => { render(template.getEventNameDisplay(historyRecord)); expect(screen.getByText('Updated move')).toBeInTheDocument(); }); + it('displays correct details when an SC is unassigned', () => { + historyRecord.changedValues = { + ...historyRecord.changedValues, + sc_assigned_id: null, + }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Counseling Completed')).toBeInTheDocument(); + expect(screen.getByText('Counselor Unassigned')).toBeInTheDocument(); + }); }); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.jsx index 1d433adf367..bcdc2533a0c 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.jsx @@ -9,5 +9,12 @@ export default { eventName: o.updateMoveTaskOrderStatus, tableName: t.moves, getEventNameDisplay: () => 'Approved move', - getDetails: () => <> Created Move Task Order (MTO) , + getDetails: ({ changedValues }) => { + return ( + <> +
Created Move Task Order (MTO)
+ {changedValues?.too_assigned_id !== undefined ?
Task Ordering Officer Unassigned
: null} + + ); + }, }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.test.jsx index 6f6f5a11f78..c7a47beb1d3 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateMoveTaskOrderStatus/updateMoveTaskOrderStatus.test.jsx @@ -27,4 +27,16 @@ describe('when given a Move approved history record', () => { expect(screen.getByText('Approved move')); expect(screen.getByText('Created Move Task Order (MTO)')); }); + + it('displays correct details when a TOO is unassigned', () => { + historyRecord.changedValues = { + ...historyRecord.changedValues, + too_assigned_id: null, + }; + + const template = getTemplate(historyRecord); + render(template.getDetails(historyRecord)); + expect(screen.getByText('Created Move Task Order (MTO)')); + expect(screen.getByText('Task Ordering Officer Unassigned')); + }); }); diff --git a/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.jsx b/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.jsx new file mode 100644 index 00000000000..82fa3b3d2c7 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; + +export default { + action: a.UPDATE, + eventName: o.updatePaymentRequestStatus, + tableName: t.moves, + getEventNameDisplay: () => 'Updated move', + getDetails: ({ changedValues }) => ( + <> +
Payment Requests Addressed
+ {changedValues?.tio_assigned_id !== undefined ?
Task Invoicing Officer Unassigned
: null} + + ), +}; diff --git a/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.test.jsx new file mode 100644 index 00000000000..4868aefe01c --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves.test.jsx @@ -0,0 +1,42 @@ +import { screen, render } from '@testing-library/react'; + +import e from 'constants/MoveHistory/EventTemplates/UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves'; +import getTemplate from 'constants/MoveHistory/TemplateManager'; + +describe('When given a completed services counseling for a move', () => { + const historyRecord = { + action: 'UPDATE', + eventName: 'updatePaymentRequestStatus', + tableName: 'moves', + }; + it('correctly matches the update mto status services counseling completed event to the proper template', () => { + const template = getTemplate(historyRecord); + expect(template).toMatchObject(e); + }); + + it('displays the proper name in the event name display column', () => { + const template = getTemplate(historyRecord); + + render(template.getEventNameDisplay(historyRecord)); + expect(screen.getByText('Updated move')).toBeInTheDocument(); + }); + + it('displays default when TIO ID is not present', () => { + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Payment Requests Addressed')).toBeInTheDocument(); + }); + + it('displays correct details when a TIO is unassigned', () => { + historyRecord.changedValues = { + ...historyRecord.changedValues, + tio_assigned_id: null, + }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Payment Requests Addressed')).toBeInTheDocument(); + expect(screen.getByText('Task Invoicing Officer Unassigned')).toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.jsx b/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.jsx new file mode 100644 index 00000000000..8733ec0bc30 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; +import { getMtoShipmentLabel } from 'utils/formatMtoShipment'; +import LabeledDetails from 'pages/Office/MoveHistory/LabeledDetails'; +import { PAYMENT_SERVICE_ITEM_STATUS } from 'shared/constants'; + +const formatChangedValues = (historyRecord) => { + const newChangedValues = { + ...historyRecord.changedValues, + ...getMtoShipmentLabel(historyRecord), + }; + + // Removed unneeded values to avoid clutter in audit log + if (newChangedValues.status === 'APPROVED' || newChangedValues.status === 'REQUESTED') { + delete newChangedValues.rejection_reason; + } + delete newChangedValues.status; + const newHistoryRecord = { ...historyRecord }; + delete newHistoryRecord.changedValues.status; + return { ...newHistoryRecord, changedValues: newChangedValues }; +}; + +export default { + action: a.UPDATE, + eventName: o.updatePaymentServiceItemStatus, + tableName: t.payment_service_items, + getEventNameDisplay: (historyRecord) => { + let actionPrefix = ''; + + /** + * IF there is a rejection_reason present in the changedValues, then either the reason was updated (in which case the status will be undefined) + * OR it was just rejected, wither way we want the rejected prefix, second || condition is a "just in case" check, not sure if there's a state + * where status would be updated but not rejection_reason + */ + if ( + ('rejection_reason' in historyRecord.changedValues && + historyRecord.changedValues.rejection_reason !== null && + historyRecord.changedValues.status !== 'APPROVED' && + historyRecord.changedValues.status !== 'REQUESTED') || + historyRecord.changedValues.status === PAYMENT_SERVICE_ITEM_STATUS.DENIED + ) { + actionPrefix = 'Rejected'; + } else if (historyRecord.changedValues.status === PAYMENT_SERVICE_ITEM_STATUS.APPROVED) { + actionPrefix = 'Approved'; + } else { + actionPrefix = 'Updated'; + } + return
{actionPrefix} Payment Service Item
; + }, + getDetails: (historyRecord) => { + return ; + }, +}; diff --git a/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.test.jsx new file mode 100644 index 00000000000..1d0c3216a91 --- /dev/null +++ b/src/constants/MoveHistory/EventTemplates/UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus.test.jsx @@ -0,0 +1,88 @@ +import { render, screen } from '@testing-library/react'; + +import getTemplate from 'constants/MoveHistory/TemplateManager'; +import o from 'constants/MoveHistory/UIDisplay/Operations'; +import a from 'constants/MoveHistory/Database/Actions'; +import t from 'constants/MoveHistory/Database/Tables'; + +describe('When approving/rejecting a payment service item', () => { + const rejectPaymentServiceItemRecord = { + action: a.UPDATE, + actionTstampClk: '2025-01-10T19:44:31.255Z', + actionTstampStm: '2025-01-10T19:44:31.253Z', + actionTstampTx: '2025-01-10T19:44:31.220Z', + context: [ + { + shipment_type: 'PPM', + shipment_locator: 'RQ38D4-01', + shipment_id_abbr: 'f10be', + }, + ], + changedValues: { + rejection_reason: 'Some reason', + status: 'DENIED', + }, + eventName: o.updatePaymentServiceItemStatus, + tableName: t.payment_service_items, + id: '2419f1db-3f8b-4823-974f-9aa4edb753da', + objectId: 'eee30fb1-dc66-4821-a17c-2ecf431ceb9d', + oldValues: { + id: 'eee30fb1-dc66-4821-a17c-2ecf431ceb9d', + ppm_shipment_id: '86329c14-564b-4580-94b9-8a2e80bccefc', + reason: null, + status: null, + }, + }; + + const approvePaymentServiceItemRecord = { ...rejectPaymentServiceItemRecord }; + approvePaymentServiceItemRecord.changedValues = { + status: 'APPROVED', + }; + + it('displays an approved payment service item', () => { + const template = getTemplate(approvePaymentServiceItemRecord); + + render(template.getEventNameDisplay(approvePaymentServiceItemRecord)); + expect(screen.getByText('Approved Payment Service Item')).toBeInTheDocument(); + + render(template.getDetails(approvePaymentServiceItemRecord)); + expect(screen.getByText('PPM shipment #RQ38D4-01')).toBeInTheDocument(); + }); + + it('displays an updated payment service item', () => { + const updatedServiceItemRecord = { ...approvePaymentServiceItemRecord }; + delete updatedServiceItemRecord.changedValues.status; + delete updatedServiceItemRecord.changedValues.rejection_reason; + const template = getTemplate(updatedServiceItemRecord); + + render(template.getEventNameDisplay(updatedServiceItemRecord)); + expect(screen.getByText('Updated Payment Service Item')).toBeInTheDocument(); + + render(template.getDetails(updatedServiceItemRecord)); + expect(screen.getByText('PPM shipment #RQ38D4-01')).toBeInTheDocument(); + }); + + it('displays a rejected payment service item and the rejection reason', () => { + const template = getTemplate(rejectPaymentServiceItemRecord); + + render(template.getEventNameDisplay(rejectPaymentServiceItemRecord)); + expect(screen.getByText('Rejected Payment Service Item')).toBeInTheDocument(); + + render(template.getDetails(rejectPaymentServiceItemRecord)); + expect(screen.getByText('Reason')).toBeInTheDocument(); + expect(screen.getByText(': Some reason')).toBeInTheDocument(); + }); + + it('displays a cleared payment service item with no unneeded information', () => { + const clearedServiceItem = rejectPaymentServiceItemRecord; + clearedServiceItem.changedValues.status = 'REQUESTED'; + const template = getTemplate(clearedServiceItem); + + render(template.getEventNameDisplay(clearedServiceItem)); + expect(screen.getByText('Updated Payment Service Item')).toBeInTheDocument(); + + render(template.getDetails(clearedServiceItem)); + expect(screen.queryByText('Reason')).not.toBeInTheDocument(); + expect(screen.queryByText(': Some reason')).not.toBeInTheDocument(); + }); +}); diff --git a/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.jsx b/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.jsx index 799beef3b0a..0bd4ffcedd8 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.jsx @@ -10,5 +10,17 @@ export default { eventName: o.updateMTOServiceItemStatus, tableName: t.moves, getEventNameDisplay: () => 'Updated move', - getDetails: (historyRecord) => , + getDetails: (historyRecord) => { + return ( + <> + + {historyRecord?.changedValues?.too_assigned_id !== undefined ? ( + <> +
Service Items Addressed
+
Task Ordering Officer Unassigned
+ + ) : null} + + ); + }, }; diff --git a/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.test.jsx b/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.test.jsx index 6660baa258e..94d6497cd49 100644 --- a/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.test.jsx +++ b/src/constants/MoveHistory/EventTemplates/UpdateServiceItemStatus/updateServiceItemStatusUpdateMove.test.jsx @@ -25,4 +25,16 @@ describe('when given a update service item status, update move history record', expect(screen.getByText(value)).toBeInTheDocument(); }); }); + + it('displays correct details when a TOO is unassigned', () => { + historyRecord.changedValues = { + ...historyRecord.changedValues, + too_assigned_id: null, + }; + const template = getTemplate(historyRecord); + + render(template.getDetails(historyRecord)); + expect(screen.getByText('Service Items Addressed')).toBeInTheDocument(); + expect(screen.getByText('Task Ordering Officer Unassigned')).toBeInTheDocument(); + }); }); diff --git a/src/constants/MoveHistory/EventTemplates/index.js b/src/constants/MoveHistory/EventTemplates/index.js index 249eaf2dad3..68760edcd43 100644 --- a/src/constants/MoveHistory/EventTemplates/index.js +++ b/src/constants/MoveHistory/EventTemplates/index.js @@ -111,4 +111,13 @@ export { default as moveCancelerPPMShipments } from './MoveCanceler/MoveCanceler export { default as cancelMove } from './CancelMove/CancelMove'; export { default as cancelMoveMTOShipments } from './CancelMove/CancelMoveMTOShipments'; export { default as cancelMovePPMShipments } from './CancelMove/CancelMovePPMShipments'; +export { default as updateAssignedOfficeUser } from './UpdateAssignedOfficeUser/UpdateAssignedOfficeUser'; +export { default as deleteAssignedOfficeUser } from './UpdateAssignedOfficeUser/DeleteAssignedOfficeUser'; +export { default as UpdatePaymentRequestStatusMoves } from './UpdatePaymentRequestStatus/UpdatePaymentRequestStatusMoves'; export { default as reviewShipmentAddressUpdate } from './ReviewShipmentAddressUpdate/reviewShipmentAddressUpdate'; +export { default as FinishDocumentReviewMoves } from './FinishDocumentReview/FinishDocumentReviewMoves'; +export { default as approveShipments } from './ApproveShipments/approveShipments'; +export { default as approveShipmentsUpdateAllowances } from './ApproveShipments/approveShipmentsUpdateAllowances'; +export { default as approveShipmentsApproveMove } from './ApproveShipments/approveShipmentsApproveMove'; +export { default as approveShipmentsServiceItem } from './ApproveShipments/approveShipmentsServiceItem'; +export { default as updatePaymentServiceItemStatus } from './UpdatePaymentServiceItem/UpdatePaymentServiceItemStatus'; diff --git a/src/constants/MoveHistory/UIDisplay/Operations.js b/src/constants/MoveHistory/UIDisplay/Operations.js index a74bccc3603..bbe9d846345 100644 --- a/src/constants/MoveHistory/UIDisplay/Operations.js +++ b/src/constants/MoveHistory/UIDisplay/Operations.js @@ -2,6 +2,7 @@ export default { acknowledgeExcessWeightRisk: 'acknowledgeExcessWeightRisk', acknowledgeExcessUnaccompaniedBaggageWeightRisk: 'acknowledgeExcessUnaccompaniedBaggageWeightRisk', approveShipment: 'approveShipment', // ghc.yaml + approveShipments: 'approveShipments', // ghc.yaml approveShipmentDiversion: 'approveShipmentDiversion', counselingUpdateAllowance: 'counselingUpdateAllowance', // ghc.yaml counselingUpdateOrder: 'counselingUpdateOrder', // ghc.yaml @@ -35,6 +36,7 @@ export default { updateOrder: 'updateOrder', // ghc.yaml updateOrders: 'updateOrders', // internal.yaml updatePaymentRequestStatus: 'updatePaymentRequestStatus', + updatePaymentServiceItemStatus: 'updatePaymentServiceItemStatus', updateReweigh: 'updateReweigh', updateServiceItemStatus: 'updateMTOServiceItemStatus', updateServiceItemSitEntryDate: 'updateServiceItemSitEntryDate', // ghc.yaml @@ -66,5 +68,7 @@ export default { addAppealToViolation: 'addAppealToViolation', // ghc.yaml addAppealToSeriousIncident: 'addAppealToSeriousIncident', // ghc.yaml cancelMove: 'cancelMove', // internal.yaml + updateAssignedOfficeUser: 'updateAssignedOfficeUser', // ghc.yaml + deleteAssignedOfficeUser: 'deleteAssignedOfficeUser', // ghc.yaml reviewShipmentAddressUpdate: 'reviewShipmentAddressUpdate', // ghc.yaml }; diff --git a/src/constants/prime.js b/src/constants/prime.js index 267156031b9..358ea7508de 100644 --- a/src/constants/prime.js +++ b/src/constants/prime.js @@ -4,13 +4,13 @@ import { serviceItemCodes } from 'content/serviceItems'; export const createServiceItemModelTypes = { MTOServiceItemOriginSIT: 'MTOServiceItemOriginSIT', MTOServiceItemDestSIT: 'MTOServiceItemDestSIT', - MTOServiceItemShuttle: 'MTOServiceItemShuttle', + MTOServiceItemDomesticShuttle: 'MTOServiceItemDomesticShuttle', MTOServiceItemDomesticCrating: 'MTOServiceItemDomesticCrating', MTOServiceItemInternationalCrating: 'MTOServiceItemInternationalCrating', MTOServiceItemInternationalShuttle: 'MTOServiceItemInternationalShuttle', }; -export const shuttleServiceItemCodeOptions = [ +export const domesticShuttleServiceItemCodeOptions = [ { value: serviceItemCodes.DOSHUT, key: SERVICE_ITEM_CODES.DOSHUT }, { value: serviceItemCodes.DDSHUT, key: SERVICE_ITEM_CODES.DDSHUT }, ]; diff --git a/src/constants/serviceItems.js b/src/constants/serviceItems.js index 213afb45f38..f0d7c5cccff 100644 --- a/src/constants/serviceItems.js +++ b/src/constants/serviceItems.js @@ -185,6 +185,8 @@ const SERVICE_ITEMS_ALLOWED_UPDATE = [ SERVICE_ITEM_CODES.DDFSIT, SERVICE_ITEM_CODES.DOSFSC, SERVICE_ITEM_CODES.DDSFSC, + SERVICE_ITEM_CODES.IDSHUT, + SERVICE_ITEM_CODES.IOSHUT, SERVICE_ITEM_CODES.PODFSC, SERVICE_ITEM_CODES.POEFSC, ]; diff --git a/src/hooks/queries.js b/src/hooks/queries.js index 749f7602a91..1129a3e7901 100644 --- a/src/hooks/queries.js +++ b/src/hooks/queries.js @@ -572,9 +572,10 @@ export const useMovesQueueQueries = ({ currentPage = PAGINATION_PAGE_DEFAULT, currentPageSize = PAGINATION_PAGE_SIZE_DEFAULT, viewAsGBLOC, + activeRole, }) => { const { data = {}, ...movesQueueQuery } = useQuery( - [MOVES_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC }], + [MOVES_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC, activeRole }], ({ queryKey }) => getMovesQueue(...queryKey), ); const { isLoading, isError, isSuccess } = movesQueueQuery; @@ -616,11 +617,12 @@ export const useServicesCounselingQueuePPMQueries = ({ currentPage = PAGINATION_PAGE_DEFAULT, currentPageSize = PAGINATION_PAGE_SIZE_DEFAULT, viewAsGBLOC, + activeRole, }) => { const { data = {}, ...servicesCounselingQueueQuery } = useQuery( [ SERVICES_COUNSELING_QUEUE, - { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: true, viewAsGBLOC }, + { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: true, viewAsGBLOC, activeRole }, ], ({ queryKey }) => getServicesCounselingPPMQueue(...queryKey), ); @@ -642,11 +644,12 @@ export const useServicesCounselingQueueQueries = ({ currentPage = PAGINATION_PAGE_DEFAULT, currentPageSize = PAGINATION_PAGE_SIZE_DEFAULT, viewAsGBLOC, + activeRole, }) => { const { data = {}, ...servicesCounselingQueueQuery } = useQuery( [ SERVICES_COUNSELING_QUEUE, - { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: false, viewAsGBLOC }, + { sort, order, filters, currentPage, currentPageSize, needsPPMCloseout: false, viewAsGBLOC, activeRole }, ], ({ queryKey }) => getServicesCounselingQueue(...queryKey), ); @@ -668,9 +671,10 @@ export const usePaymentRequestQueueQueries = ({ currentPage = PAGINATION_PAGE_DEFAULT, currentPageSize = PAGINATION_PAGE_SIZE_DEFAULT, viewAsGBLOC, + activeRole, }) => { const { data = {}, ...paymentRequestsQueueQuery } = useQuery( - [PAYMENT_REQUESTS_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC }], + [PAYMENT_REQUESTS_QUEUE, { sort, order, filters, currentPage, currentPageSize, viewAsGBLOC, activeRole }], ({ queryKey }) => getPaymentRequestsQueue(...queryKey), ); diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx index 9aeb51ea0f9..f87651ad31a 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserList.jsx @@ -1,41 +1,126 @@ import React from 'react'; -import { Datagrid, DateField, Filter, List, ReferenceField, TextField, TextInput, TopToolbar } from 'react-admin'; +import { + ArrayField, + Datagrid, + DateField, + Filter, + List, + ReferenceField, + TextField, + TextInput, + TopToolbar, + useRecordContext, + downloadCSV, + useDataProvider, + ExportButton, + useListController, +} from 'react-admin'; +import jsonExport from 'jsonexport/dist'; import AdminPagination from 'scenes/SystemAdmin/shared/AdminPagination'; -// Overriding the default toolbar -const ListActions = () => { - return ; -}; - -const RequestedOfficeUserListFilter = () => ( - - +const RequestedOfficeUserListFilter = (props) => ( + + + + ); const defaultSort = { field: 'createdAt', order: 'DESC' }; -const RequestedOfficeUserList = () => ( - } - perPage={25} - sort={defaultSort} - filters={} - actions={} - > - - - - - - - - - - - - -); +const UserRolesToString = (user) => { + const { roles } = user; + + let roleStr = ''; + for (let i = 0; i < roles.length; i += 1) { + roleStr += roles[i].roleName; + + if (i < roles.length - 1) { + roleStr += ', '; + } + } + + return roleStr; +}; + +const RolesField = () => { + const record = useRecordContext(); + return
{UserRolesToString(record)}
; +}; + +const ListActions = () => { + const { total, resource, sort, filterValues } = useListController(); + const dataProvider = useDataProvider(); + + const exporter = async (users) => { + const officeObjects = {}; + const offices = await dataProvider.getMany('offices'); + offices.data.forEach((office) => { + if (!officeObjects[`${office.id}`]) { + officeObjects[`${office.id}`] = office; + } + }); + + const usersWithTransportationOfficeName = users.map((user) => ({ + ...user, + transportationOfficeName: officeObjects[user.transportationOfficeId]?.name, + })); + + const usersForExport = usersWithTransportationOfficeName.map((user) => { + const { id, email, firstName, lastName, transportationOfficeName, status, createdAt } = user; + const userRoles = UserRolesToString(user); + return { + Id: id, + Email: email, + 'First Name': firstName, + 'Last Name': lastName, + 'Transportation Office': transportationOfficeName, + Status: status, + 'Requested On': createdAt, + Roles: userRoles, + }; + }); + + // convert data to csv and download + jsonExport(usersForExport, {}, (err, csv) => { + if (err) throw err; + downloadCSV(csv, 'requested_office_users'); + }); + }; + + return ( + + + + ); +}; + +const RequestedOfficeUserList = () => { + return ( + } + perPage={25} + sort={defaultSort} + filters={} + actions={} + > + + + + + + + + + + + + + + + + ); +}; export default RequestedOfficeUserList; diff --git a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss index 974aa69bf7f..d2b9adfc325 100644 --- a/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss +++ b/src/pages/Admin/RequestedOfficeUsers/RequestedOfficeUserShow.module.scss @@ -52,4 +52,6 @@ margin-left: 15px; margin-right: 15px; margin-bottom: 10px; -} \ No newline at end of file +} + +ul { list-style-type: none; } \ No newline at end of file diff --git a/src/pages/MyMove/Home/MoveHome.jsx b/src/pages/MyMove/Home/MoveHome.jsx index 69afb201a28..39e021443e2 100644 --- a/src/pages/MyMove/Home/MoveHome.jsx +++ b/src/pages/MyMove/Home/MoveHome.jsx @@ -434,9 +434,15 @@ const MoveHome = ({ serviceMemberMoves, isProfileComplete, serviceMember, signed
-
Weight allowance
+
Standard weight allowance
{formatWeight(orders.authorizedWeight)}
+ {orders?.entitlement?.weight_restriction > 0 && ( +
+
Weight restriction
+
{formatWeight(orders?.entitlement?.weight_restriction)}
+
+ )} {orders?.entitlement?.ub_allowance > 0 && (
UB allowance
diff --git a/src/pages/MyMove/Home/MoveHome.test.jsx b/src/pages/MyMove/Home/MoveHome.test.jsx index aa1da0f83ad..82e7c5b53ba 100644 --- a/src/pages/MyMove/Home/MoveHome.test.jsx +++ b/src/pages/MyMove/Home/MoveHome.test.jsx @@ -1196,10 +1196,11 @@ describe('Home component', () => { wrapper = mountMoveHomeWithProviders(defaultPropsOrdersWithUBAllowance); }); await waitFor(() => { - expect(wrapper.text()).toContain('Weight allowance'); + expect(wrapper.text()).toContain('Standard weight allowance'); expect(wrapper.text()).toContain('11,000 lbs'); expect(wrapper.text()).toContain('UB allowance'); expect(wrapper.text()).toContain('2,000 lbs'); + expect(wrapper.text()).not.toContain('Weight restriction'); }); const ubToolTip = wrapper.find('ToolTip'); diff --git a/src/pages/MyMove/Home/index.jsx b/src/pages/MyMove/Home/index.jsx index 33f7d5bfb34..5c26f999d5d 100644 --- a/src/pages/MyMove/Home/index.jsx +++ b/src/pages/MyMove/Home/index.jsx @@ -265,9 +265,15 @@ export class Home extends Component {
-
Weight allowance
+
Standard weight allowance
{formatWeight(orders.authorizedWeight)}.
+ {orders?.entitlement?.weight_restriction > 0 && ( +
+
Weight restriction
+
{formatWeight(orders?.entitlement?.weight_restriction)}
+
+ )} {move.locator && (
Move code
diff --git a/src/pages/MyMove/MovingInfo.jsx b/src/pages/MyMove/MovingInfo.jsx index ebcb960407f..5ea1a6d7b2c 100644 --- a/src/pages/MyMove/MovingInfo.jsx +++ b/src/pages/MyMove/MovingInfo.jsx @@ -84,8 +84,9 @@ export class MovingInfo extends Component { +

If you are moving to an administratively restricted HHG weight location this amount may be less.

You will have to pay for any excess weight above this allowance, so work hard to make sure you stay within your weight limit. diff --git a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx index 1786b4525ff..c9d08481f3b 100644 --- a/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx +++ b/src/pages/Office/HeadquartersQueues/HeadquartersQueues.jsx @@ -38,7 +38,7 @@ import { milmoveLogger } from 'utils/milmoveLog'; import ConnectedFlashMessage from 'containers/FlashMessage/FlashMessage'; import CustomerSearchForm from 'components/CustomerSearchForm/CustomerSearchForm'; -const HeadquartersQueue = ({ isQueueManagementFFEnabled }) => { +const HeadquartersQueue = ({ isQueueManagementFFEnabled, activeRole }) => { const navigate = useNavigate(); const { queueType } = useParams(); const [search, setSearch] = useState({ moveCode: null, dodID: null, customerName: null, paymentRequestCode: null }); @@ -247,6 +247,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled }) => { csvExportQueueFetcher={getMovesQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + activeRole={activeRole} />

); @@ -273,6 +274,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled }) => { csvExportQueueFetcher={getPaymentRequestsQueue} csvExportQueueFetcherKey="queuePaymentRequests" sessionStorageKey={queueType} + activeRole={activeRole} />
); @@ -299,6 +301,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled }) => { csvExportQueueFetcher={getServicesCounselingPPMQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + activeRole={activeRole} />
); @@ -326,6 +329,7 @@ const HeadquartersQueue = ({ isQueueManagementFFEnabled }) => { csvExportQueueFetcher={getServicesCounselingQueue} csvExportQueueFetcherKey="queueMoves" sessionStorageKey={queueType} + activeRole={activeRole} />
); diff --git a/src/pages/Office/MoveAllowances/MoveAllowances.jsx b/src/pages/Office/MoveAllowances/MoveAllowances.jsx index 9adf62bdc39..5e7057c752f 100644 --- a/src/pages/Office/MoveAllowances/MoveAllowances.jsx +++ b/src/pages/Office/MoveAllowances/MoveAllowances.jsx @@ -43,6 +43,11 @@ const validationSchema = Yup.object({ .min(0, 'Storage in transit (days) must be greater than or equal to 0') .transform((value) => (Number.isNaN(value) ? 0 : value)) .notRequired(), + weightRestriction: Yup.number() + .min(1, 'Weight restriction must be greater than 0') + .max(18000, 'Weight restriction cannot exceed 18,000 lbs') + .transform((value) => (Number.isNaN(value) ? 0 : value)) + .notRequired(), }); const MoveAllowances = () => { @@ -98,6 +103,7 @@ const MoveAllowances = () => { organizationalClothingAndIndividualEquipment, storageInTransit, gunSafe, + weightRestriction, accompaniedTour, dependentsTwelveAndOver, dependentsUnderTwelve, @@ -118,6 +124,7 @@ const MoveAllowances = () => { organizationalClothingAndIndividualEquipment, storageInTransit: Number(storageInTransit), gunSafe, + weightRestriction: Number(weightRestriction), accompaniedTour, dependentsTwelveAndOver: Number(dependentsTwelveAndOver), dependentsUnderTwelve: Number(dependentsUnderTwelve), @@ -133,6 +140,7 @@ const MoveAllowances = () => { requiredMedicalEquipmentWeight, organizationalClothingAndIndividualEquipment, gunSafe, + weightRestriction, storageInTransit, dependentsUnderTwelve, dependentsTwelveAndOver, @@ -148,6 +156,7 @@ const MoveAllowances = () => { requiredMedicalEquipmentWeight: `${requiredMedicalEquipmentWeight}`, organizationalClothingAndIndividualEquipment, gunSafe, + weightRestriction: `${weightRestriction}`, storageInTransit: `${storageInTransit}`, accompaniedTour, dependentsUnderTwelve: `${dependentsUnderTwelve}`, diff --git a/src/pages/Office/MoveDetails/MoveDetails.jsx b/src/pages/Office/MoveDetails/MoveDetails.jsx index ad79729da80..e02fd6ae183 100644 --- a/src/pages/Office/MoveDetails/MoveDetails.jsx +++ b/src/pages/Office/MoveDetails/MoveDetails.jsx @@ -24,7 +24,13 @@ import CancelMoveConfirmationModal from 'components/ConfirmationModals/CancelMov import ApprovedRequestedShipments from 'components/Office/RequestedShipments/ApprovedRequestedShipments'; import SubmittedRequestedShipments from 'components/Office/RequestedShipments/SubmittedRequestedShipments'; import { useMoveDetailsQueries } from 'hooks/queries'; -import { updateMoveStatus, updateMTOShipmentStatus, cancelMove, updateFinancialFlag } from 'services/ghcApi'; +import { + updateMoveStatus, + updateMTOShipmentStatus, + cancelMove, + updateFinancialFlag, + updateMultipleShipmentStatus, +} from 'services/ghcApi'; import LeftNav from 'components/LeftNav/LeftNav'; import LeftNavTag from 'components/LeftNavTag/LeftNavTag'; import Restricted from 'components/Restricted/Restricted'; @@ -133,7 +139,7 @@ const MoveDetails = ({ }, }); - const { mutate: mutateMTOShipmentStatus } = useMutation(updateMTOShipmentStatus, { + const { mutateAsync: mutateMTOShipmentStatus } = useMutation(updateMTOShipmentStatus, { onSuccess: (updatedMTOShipment) => { mtoShipments[mtoShipments.findIndex((shipment) => shipment.id === updatedMTOShipment.id)] = updatedMTOShipment; queryClient.setQueryData([MTO_SHIPMENTS, updatedMTOShipment.moveTaskOrderID, false], mtoShipments); @@ -142,6 +148,17 @@ const MoveDetails = ({ }, }); + const { mutateAsync: mutateMultipleShipmentStatuses } = useMutation(updateMultipleShipmentStatus, { + onSuccess: (updatedMTOShipments) => { + updatedMTOShipments.forEach((updatedMTOShipment) => { + mtoShipments[mtoShipments.findIndex((shipment) => shipment.id === updatedMTOShipment.id)] = updatedMTOShipment; + queryClient.setQueryData([MTO_SHIPMENTS, updatedMTOShipment.moveTaskOrderID, false], mtoShipments); + queryClient.invalidateQueries([MTO_SHIPMENTS, updatedMTOShipment.moveTaskOrderID]); + queryClient.invalidateQueries([MTO_SERVICE_ITEMS, updatedMTOShipment.moveTaskOrderID]); + }); + }, + }); + const { mutate: mutateCancelMove } = useMutation(cancelMove, { onSuccess: (data) => { queryClient.setQueryData([MOVES, data.locator], data); @@ -431,6 +448,7 @@ const MoveDetails = ({ requiredMedicalEquipmentWeight: allowances.requiredMedicalEquipmentWeight, organizationalClothingAndIndividualEquipment: allowances.organizationalClothingAndIndividualEquipment, gunSafe: allowances.gunSafe, + weightRestriction: allowances.weightRestriction, dependentsUnderTwelve: allowances.dependentsUnderTwelve, dependentsTwelveAndOver: allowances.dependentsTwelveAndOver, accompaniedTour: allowances.accompaniedTour, @@ -585,6 +603,7 @@ const MoveDetails = ({ customerInfo={customerInfo} approveMTO={mutateMoveStatus} approveMTOShipment={mutateMTOShipmentStatus} + approveMultipleShipments={mutateMultipleShipmentStatuses} moveTaskOrder={move} missingRequiredOrdersInfo={hasMissingOrdersRequiredInfo} handleAfterSuccess={navigate} @@ -659,7 +678,7 @@ const MoveDetails = ({ } shipmentsInfoNonPpm={shipmentsInfoNonPPM} > - +
diff --git a/src/pages/Office/MoveHistory/PaymentDetails.jsx b/src/pages/Office/MoveHistory/PaymentDetails.jsx index 65af4dbe90c..f1b38320d23 100644 --- a/src/pages/Office/MoveHistory/PaymentDetails.jsx +++ b/src/pages/Office/MoveHistory/PaymentDetails.jsx @@ -24,6 +24,13 @@ const filterContextStatus = (context, statusToFilter) => {
{value.name}
{price.toFixed(2)}
+
+ {value.status === PAYMENT_SERVICE_ITEM_STATUS.DENIED ? ( +
+ Rejection Reason: + {value?.rejection_reason} +
+ ) : null}
, ); } diff --git a/src/pages/Office/MoveHistory/PaymentDetails.module.scss b/src/pages/Office/MoveHistory/PaymentDetails.module.scss index ba62a69fd09..8e347f56909 100644 --- a/src/pages/Office/MoveHistory/PaymentDetails.module.scss +++ b/src/pages/Office/MoveHistory/PaymentDetails.module.scss @@ -13,6 +13,7 @@ font-weight: 500; display: flex; justify-content: space-between; + flex-wrap: wrap; } .statusRow { display: flex; @@ -24,4 +25,11 @@ .rejectTimes { color: $error; } + .break { + flex-basis: 100%; + height: 0; + } + .rejectionReason { + text-indent: 1rem; + } } diff --git a/src/pages/Office/MoveHistory/PaymentDetails.test.jsx b/src/pages/Office/MoveHistory/PaymentDetails.test.jsx index fca870071c2..f9f38895d70 100644 --- a/src/pages/Office/MoveHistory/PaymentDetails.test.jsx +++ b/src/pages/Office/MoveHistory/PaymentDetails.test.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, act } from '@testing-library/react'; import PaymentDetails from './PaymentDetails'; @@ -47,4 +47,26 @@ describe('PaymentDetails', () => { expect(screen.getByText(156.78, { exact: false })).toBeInTheDocument(); }); + + describe('rejected service items', () => { + const context = [ + { + name: 'Domestic uncrating', + price: '5555', + status: 'DENIED', + rejection_reason: 'some reason', + }, + ]; + it('renders a rejected service item and its rejection reason', async () => { + render(); + + expect(screen.getByText('Domestic uncrating')).toBeInTheDocument(); + + expect(screen.getByText('Rejection Reason:')).toBeInTheDocument(); + await act(() => { + screen.getByText('Rejection Reason:').click(); + }); + expect(screen.getByText('some reason')).toBeVisible(); + }); + }); }); diff --git a/src/pages/Office/MoveQueue/MoveQueue.jsx b/src/pages/Office/MoveQueue/MoveQueue.jsx index 36e090a48de..8e327ee61e5 100644 --- a/src/pages/Office/MoveQueue/MoveQueue.jsx +++ b/src/pages/Office/MoveQueue/MoveQueue.jsx @@ -193,7 +193,7 @@ export const columns = (moveLockFlag, isQueueManagementEnabled, showBranchFilter return cols; }; -const MoveQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBulkAssignmentFFEnabled }) => { +const MoveQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBulkAssignmentFFEnabled, activeRole }) => { const navigate = useNavigate(); const { queueType } = useParams(); const [search, setSearch] = useState({ moveCode: null, dodID: null, customerName: null, paymentRequestCode: null }); @@ -348,6 +348,7 @@ const MoveQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBulkAssignmen key={queueType} isSupervisor={supervisor} isBulkAssignmentFFEnabled={isBulkAssignmentFFEnabled} + activeRole={activeRole} />
); diff --git a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx index a46518a2627..c43cf7bfbe6 100644 --- a/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx +++ b/src/pages/Office/PaymentRequestQueue/PaymentRequestQueue.jsx @@ -191,7 +191,7 @@ export const columns = (moveLockFlag, isQueueManagementEnabled, showBranchFilter return cols; }; -const PaymentRequestQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBulkAssignmentFFEnabled }) => { +const PaymentRequestQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBulkAssignmentFFEnabled, activeRole }) => { const { queueType } = useParams(); const navigate = useNavigate(); const [search, setSearch] = useState({ moveCode: null, dodID: null, customerName: null, paymentRequestCode: null }); @@ -334,6 +334,7 @@ const PaymentRequestQueue = ({ isQueueManagementFFEnabled, userPrivileges, isBul key={queueType} isSupervisor={supervisor} isBulkAssignmentFFEnabled={isBulkAssignmentFFEnabled} + activeRole={activeRole} />
); diff --git a/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx b/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx index eb57eefb665..972b01d05ce 100644 --- a/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx +++ b/src/pages/Office/ServicesCounselingAddOrders/ServicesCounselingAddOrders.test.jsx @@ -8,6 +8,7 @@ import { MockProviders } from 'testUtils'; import { counselingCreateOrder } from 'services/ghcApi'; import { setCanAddOrders } from 'store/general/actions'; import { isBooleanFlagEnabled } from 'utils/featureFlags'; +import { servicesCounselingRoutes } from 'constants/routes'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -23,6 +24,20 @@ jest.mock('utils/featureFlags', () => ({ jest.mock('services/ghcApi', () => ({ ...jest.requireActual('services/ghcApi'), counselingCreateOrder: jest.fn().mockImplementation(() => Promise.resolve()), + showCounselingOffices: jest.fn().mockImplementation(() => + Promise.resolve({ + body: [ + { + id: '3e937c1f-5539-4919-954d-017989130584', + name: 'Albuquerque AFB', + }, + { + id: 'fa51dab0-4553-4732-b843-1f33407f77bc', + name: 'Glendale Luke AFB', + }, + ], + }), + ), })); jest.mock('services/internalApi', () => ({ @@ -79,6 +94,7 @@ jest.mock('components/LocationSearchBox/api', () => ({ id: '7d123884-7c1b-4611-92ae-e8d43ca03ad9', name: 'Hill AFB', updated_at: '2021-02-11T16:48:04.117Z', + provides_services_counseling: true, }, { address: { @@ -316,10 +332,13 @@ const fakeResponse = { }, }; +const mockParams = { customerId: 'ea51dab0-4553-4732-b843-1f33407f77bd' }; +const mockPath = servicesCounselingRoutes.BASE_CUSTOMERS_ORDERS_ADD_PATH; + const renderWithMocks = () => { const testProps = { customer, setCanAddOrders: jest.fn() }; render( - + , ); @@ -342,18 +361,21 @@ describe('ServicesCounselingAddOrders component', () => { const user = userEvent.setup(); - await user.selectOptions(screen.getByLabelText('Orders type'), 'PERMANENT_CHANGE_OF_STATION'); - await user.type(screen.getByLabelText('Orders date'), '08 Nov 2020'); - await user.type(screen.getByLabelText('Report by date'), '26 Nov 2020'); + await user.selectOptions(screen.getByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await user.type(screen.getByLabelText(/Orders date/), '08 Nov 2020'); + await user.type(screen.getByLabelText(/Report by date/), '26 Nov 2020'); await user.click(screen.getByLabelText('No')); - await user.selectOptions(screen.getByLabelText('Pay grade'), ['E-5']); + await user.selectOptions(screen.getByLabelText(/Pay grade/), ['E-5']); // Test Current Duty Location Search Box interaction - await user.type(screen.getByLabelText('Current duty location'), 'AFB', { delay: 500 }); + await user.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 500 }); const selectedOptionCurrent = await screen.findByText(/Altus/); await user.click(selectedOptionCurrent); - await user.type(screen.getByLabelText('New duty location'), 'AFB', { delay: 500 }); + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeFalsy(); + + await user.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 500 }); const selectedOptionNew = await screen.findByText(/Luke/); await user.click(selectedOptionNew); @@ -370,6 +392,39 @@ describe('ServicesCounselingAddOrders component', () => { }); }); + it('Displays the counseling office dropdown', async () => { + renderWithMocks(); + + counselingCreateOrder.mockImplementation(() => Promise.resolve(fakeResponse)); + + const user = userEvent.setup(); + + await user.selectOptions(screen.getByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await user.type(screen.getByLabelText(/Orders date/), '08 Nov 2020'); + await user.type(screen.getByLabelText(/Report by date/), '29 Nov 2020'); + await user.click(screen.getByLabelText('No')); + await user.selectOptions(screen.getByLabelText(/Pay grade/), ['E-5']); + + // Test Current Duty Location Search Box interaction + await user.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 500 }); + const selectedOptionCurrent = await screen.findByText(/Hill/); + await user.click(selectedOptionCurrent); + + const counselingOfficeLabel = await screen.queryByText(/Counseling office/); + expect(counselingOfficeLabel).toBeTruthy(); + + await userEvent.selectOptions(screen.getByLabelText(/Counseling office/), ['Glendale Luke AFB']); + + await user.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 500 }); + const selectedOptionNew = await screen.findByText(/Luke/); + await user.click(selectedOptionNew); + + const nextBtn = await screen.findByRole('button', { name: 'Next' }); + await waitFor(() => { + expect(nextBtn.getAttribute('disabled')).toBeFalsy(); + }); + }); + it('routes to the move details page when the next button is clicked for OCONUS orders', async () => { isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); renderWithMocks(); @@ -378,17 +433,17 @@ describe('ServicesCounselingAddOrders component', () => { const user = userEvent.setup(); - await user.selectOptions(screen.getByLabelText('Orders type'), 'PERMANENT_CHANGE_OF_STATION'); - await user.type(screen.getByLabelText('Orders date'), '08 Nov 2020'); - await user.type(screen.getByLabelText('Report by date'), '26 Nov 2020'); + await user.selectOptions(screen.getByLabelText(/Orders type/), 'PERMANENT_CHANGE_OF_STATION'); + await user.type(screen.getByLabelText(/Orders date/), '08 Nov 2020'); + await user.type(screen.getByLabelText(/Report by date/), '26 Nov 2020'); await user.click(screen.getByLabelText('No')); - await user.selectOptions(screen.getByLabelText('Pay grade'), ['E-5']); + await user.selectOptions(screen.getByLabelText(/Pay grade/), ['E-5']); - await user.type(screen.getByLabelText('Current duty location'), 'AFB', { delay: 500 }); + await user.type(screen.getByLabelText(/Current duty location/), 'AFB', { delay: 500 }); const selectedOptionCurrent = await screen.findByText(/Altus/); await user.click(selectedOptionCurrent); - await user.type(screen.getByLabelText('New duty location'), 'AFB', { delay: 500 }); + await user.type(screen.getByLabelText(/New duty location/), 'AFB', { delay: 500 }); const selectedOptionNew = await screen.findByText(/Outta This World/); await user.click(selectedOptionNew); diff --git a/src/pages/Office/ServicesCounselingMoveAllowances/ServicesCounselingMoveAllowances.jsx b/src/pages/Office/ServicesCounselingMoveAllowances/ServicesCounselingMoveAllowances.jsx index 98a85147767..d80502bcd17 100644 --- a/src/pages/Office/ServicesCounselingMoveAllowances/ServicesCounselingMoveAllowances.jsx +++ b/src/pages/Office/ServicesCounselingMoveAllowances/ServicesCounselingMoveAllowances.jsx @@ -41,8 +41,29 @@ const validationSchema = Yup.object({ .min(0, 'Storage in transit (days) must be greater than or equal to 0') .transform((value) => (Number.isNaN(value) ? 0 : value)) .notRequired(), + adminRestrictedWeightLocation: Yup.bool(), + weightRestriction: Yup.number().when('adminRestrictedWeightLocation', { + is: (value) => { + return value === true; + }, + then: (schema) => { + return schema + .required('Weight restriction is required when location is restricted') + .min(1, 'Weight restriction must be greater than 0') + .max(18000, 'Weight restriction cannot exceed 18,000 lbs') + .transform((value) => { + return Number.isNaN(value) ? 0 : value; + }); + }, + otherwise: (schema) => { + return schema + .transform((value) => { + return Number.isNaN(value) ? 0 : value; + }) + .notRequired(); + }, + }), }); - const ServicesCounselingMoveAllowances = () => { const { moveCode } = useParams(); const navigate = useNavigate(); @@ -86,6 +107,7 @@ const ServicesCounselingMoveAllowances = () => { organizationalClothingAndIndividualEquipment, storageInTransit, gunSafe, + weightRestriction, accompaniedTour, dependentsTwelveAndOver, dependentsUnderTwelve, @@ -106,6 +128,7 @@ const ServicesCounselingMoveAllowances = () => { storageInTransit: Number(storageInTransit), organizationalClothingAndIndividualEquipment, gunSafe, + weightRestriction: Number(weightRestriction), accompaniedTour, dependentsTwelveAndOver: Number(dependentsTwelveAndOver), dependentsUnderTwelve: Number(dependentsUnderTwelve), @@ -121,6 +144,7 @@ const ServicesCounselingMoveAllowances = () => { requiredMedicalEquipmentWeight, organizationalClothingAndIndividualEquipment, gunSafe, + weightRestriction, storageInTransit, dependentsUnderTwelve, dependentsTwelveAndOver, @@ -136,10 +160,12 @@ const ServicesCounselingMoveAllowances = () => { requiredMedicalEquipmentWeight: `${requiredMedicalEquipmentWeight}`, storageInTransit: `${storageInTransit}`, gunSafe, + weightRestriction: `${weightRestriction}`, organizationalClothingAndIndividualEquipment, accompaniedTour, dependentsUnderTwelve: `${dependentsUnderTwelve}`, dependentsTwelveAndOver: `${dependentsTwelveAndOver}`, + adminRestrictedWeightLocation: false, }; return ( diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx index 7ab63c0a024..decb6c626d3 100644 --- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx +++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx @@ -387,6 +387,7 @@ const ServicesCounselingMoveDetails = ({ requiredMedicalEquipmentWeight: allowances.requiredMedicalEquipmentWeight, organizationalClothingAndIndividualEquipment: allowances.organizationalClothingAndIndividualEquipment, gunSafe: allowances.gunSafe, + weightRestriction: allowances.weightRestriction, dependentsUnderTwelve: allowances.dependentsUnderTwelve, dependentsTwelveAndOver: allowances.dependentsTwelveAndOver, accompaniedTour: allowances.accompaniedTour, @@ -878,7 +879,7 @@ const ServicesCounselingMoveDetails = ({ } ppmShipmentInfoNeedsApproval={ppmShipmentsInfoNeedsApproval} > - +
diff --git a/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx b/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx index 6518eda8080..5cb23df55aa 100644 --- a/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx +++ b/src/pages/Office/ServicesCounselingQueue/ServicesCounselingQueue.jsx @@ -433,6 +433,7 @@ const ServicesCounselingQueue = ({ isQueueManagementFFEnabled, officeUser, isBulkAssignmentFFEnabled, + activeRole, }) => { const { queueType } = useParams(); const { data, isLoading, isError } = useUserQueries(); @@ -670,6 +671,7 @@ const ServicesCounselingQueue = ({ key={queueType} isSupervisor={supervisor} isBulkAssignmentFFEnabled={isBulkAssignmentFFEnabled} + activeRole={activeRole} />
); @@ -699,6 +701,7 @@ const ServicesCounselingQueue = ({ key={queueType} isSupervisor={supervisor} isBulkAssignmentFFEnabled={isBulkAssignmentFFEnabled} + activeRole={activeRole} />
); diff --git a/src/pages/Office/index.jsx b/src/pages/Office/index.jsx index 8a945c4c7f8..69f55fbf90c 100644 --- a/src/pages/Office/index.jsx +++ b/src/pages/Office/index.jsx @@ -292,7 +292,7 @@ export class OfficeApp extends Component { end element={ - + } /> @@ -301,7 +301,10 @@ export class OfficeApp extends Component { path="/invoicing/queue" element={ - + } /> @@ -311,7 +314,10 @@ export class OfficeApp extends Component { end element={ - + } /> @@ -336,6 +342,7 @@ export class OfficeApp extends Component { userPrivileges={userPrivileges} isQueueManagementFFEnabled={queueManagementFlag} isBulkAssignmentFFEnabled={bulkAssignmentFlag} + activeRole={activeRole} /> } @@ -367,6 +374,7 @@ export class OfficeApp extends Component { isQueueManagementFFEnabled={queueManagementFlag} userPrivileges={userPrivileges} isBulkAssignmentFFEnabled={bulkAssignmentFlag} + activeRole={activeRole} /> } @@ -382,6 +390,7 @@ export class OfficeApp extends Component { isQueueManagementFFEnabled={queueManagementFlag} userPrivileges={userPrivileges} isBulkAssignmentFFEnabled={bulkAssignmentFlag} + activeRole={activeRole} /> } @@ -394,7 +403,10 @@ export class OfficeApp extends Component { end element={ - + } /> diff --git a/src/pages/PrimeUI/MoveTaskOrder/MoveDetails.jsx b/src/pages/PrimeUI/MoveTaskOrder/MoveDetails.jsx index 45881d58602..42216ae631f 100644 --- a/src/pages/PrimeUI/MoveTaskOrder/MoveDetails.jsx +++ b/src/pages/PrimeUI/MoveTaskOrder/MoveDetails.jsx @@ -24,6 +24,7 @@ import scrollToTop from 'shared/scrollToTop'; import { SERVICE_ITEMS_ALLOWED_UPDATE } from 'constants/serviceItems'; import { MoveOrderDocumentType } from 'shared/constants'; import { CHECK_SPECIAL_ORDERS_TYPES, SPECIAL_ORDERS_TYPES } from 'constants/orders'; +import { formatWeight } from 'utils/formatters'; const MoveDetails = ({ setFlashMessage }) => { const { moveCodeOrID } = useParams(); @@ -207,6 +208,14 @@ const MoveDetails = ({ setFlashMessage }) => {
Gun Safe:
{moveTaskOrder.order.entitlement.gunSafe ? 'yes' : 'no'}
+
+
Admin Restricted Weight:
+
+ {moveTaskOrder.order.entitlement.weightRestriction > 0 + ? formatWeight(moveTaskOrder.order.entitlement.weightRestriction) + : 'no'} +
+