Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion document-service/doc-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "doc-api"
version = "0.3.10"
version = "0.3.11"
description = ""
authors = ["dlovett <doug@daxiom.com>"]
license = "BSD 3"
Expand Down
3 changes: 3 additions & 0 deletions document-service/doc-api/src/doc_api/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
REPORT_TYPE_FILING = "FILING"
REPORT_TYPE_NOA = "NOA"
REPORT_TYPE_RECEIPT = "RECEIPT"
REPORT_TYPE_FILING_2 = "FILING-2"
REPORT_TYPE_FILING_3 = "FILING-3"
REPORT_TYPE_FILING_4 = "FILING-4"


def now_ts():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,34 @@ def update_product_report_info(prod_code: str, doc_service_id: str):
return resource_utils.default_exception_response(default_exception)


@bp.route("/<string:prod_code>/<string:doc_service_id>", methods=["PUT", "OPTIONS"])
@jwt.requires_auth
def replace_product_report(prod_code: str, doc_service_id: str):
"""Replace product report for application report record that is associated with the document service ID."""
try:
account_id = resource_utils.get_account_id(request)
if not is_report_authorized(jwt):
logger.error("User unuauthorized for this endpoint: not staff or service account.")
return resource_utils.unauthorized_error_response(account_id)
req_path: str = CHANGE_REQUEST_PATH_PRODUCT.format(product_code=prod_code, doc_service_id=doc_service_id)
logger.info(f"Starting replace product report record request {req_path}, account={account_id}")
if not request.get_data():
logger.error(f"Replace product report request id={doc_service_id} no payload.")
return resource_utils.bad_request_response("Replace product report request invalid: no payload.")
app_report: ApplicationReport = ApplicationReport.find_by_doc_service_id(doc_service_id, prod_code)
if not app_report:
logger.warning(f"No product report record found for {prod_code} document service id={doc_service_id}.")
return resource_utils.not_found_error_response(f"PATCH {prod_code} report information", doc_service_id)
response_json = save_replace(app_report, request.get_data())
return jsonify(response_json), HTTPStatus.CREATED, CONTENT_JSON
except DatabaseException as db_exception:
return resource_utils.db_exception_response(db_exception, account_id, "PATCH product report information")
except BusinessException as exception:
return resource_utils.business_exception_response(exception)
except Exception as default_exception: # noqa: B902; return nicer default error
return resource_utils.default_exception_response(default_exception)


@bp.route("/<string:prod_code>/<string:doc_service_id>", methods=["GET", "OPTIONS"])
@jwt.requires_auth
def get_individual_product_report(prod_code: str, doc_service_id: str):
Expand Down Expand Up @@ -424,6 +452,17 @@ def save_create(request_json: dict, raw_data) -> dict:
return report_json


def save_replace(app_report: ApplicationReport, raw_data) -> dict:
"""Replace app report record document with request binary data. Return a download link"""
logger.info(f"save_replace starting raw data size={len(raw_data)}, saving to doc storage...")
doc_link: str = save_to_doc_storage(app_report, raw_data)
app_report.save()
report_json = app_report.json
report_json["url"] = doc_link
logger.info("save_replace completed...")
return report_json


def get_report_link(app_report: ApplicationReport) -> dict:
"""Generate the report download URL link for the app report."""
report_json: dict = app_report.json
Expand Down Expand Up @@ -464,9 +503,11 @@ def get_report_links(reports_json: list, product_code: str) -> list:
for report_json in reports_json:
storage_name: str = report_json.get("url")
report_type: str = report_json.get("reportType")
if storage_name and report_type in (model_utils.REPORT_TYPE_FILING, model_utils.REPORT_TYPE_NOA):
if storage_name and (
report_type == model_utils.REPORT_TYPE_NOA or report_type.startswith(model_utils.REPORT_TYPE_FILING)
):
report_json["url"] = ""
elif storage_name and report_type not in (model_utils.REPORT_TYPE_FILING, model_utils.REPORT_TYPE_NOA):
elif storage_name:
logger.debug(f"getting link for type={storage_type} name={storage_name}...")
doc_link = GoogleStorageService.get_document_link(storage_name, storage_type, 2)
report_json["url"] = doc_link
Expand All @@ -475,10 +516,15 @@ def get_report_links(reports_json: list, product_code: str) -> list:

def is_certified_copy_request(report_data: ApplicationReport, certified_copy) -> bool:
"""Verify a report request is for a certified copy of the report."""
if not report_data.report_type or report_data.report_type not in (
# Allow certified copies of any application filing report where the report type starts with "FILING".
if not report_data.report_type:
return False
if report_data.report_type not in (
model_utils.REPORT_TYPE_NOA,
model_utils.REPORT_TYPE_FILING,
):
) and not str(
report_data.report_type
).startswith(model_utils.REPORT_TYPE_FILING):
return False
return certified_copy and bool(certified_copy) and bool(certified_copy) is True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@

from doc_api.models import ApplicationReport, Document
from doc_api.models import utils as model_utils
from doc_api.utils.logging import logger
from doc_api.resources.v1.application_reports import is_certified_copy_request
from doc_api.services.authz import BC_REGISTRY, COLIN_ROLE, STAFF_ROLE, SYSTEM_ROLE
from doc_api.utils.logging import logger
from tests.unit.services.utils import (
create_header_account,
create_header_account_report,
Expand All @@ -49,13 +50,14 @@
"consumerReferenceId": "3333001"
}
TEST_DATAFILE = "tests/unit/services/unit_test.pdf"
TEST_DATAFILE_FILING = "tests/unit/reports/data/filing.pdf"
TEST_DATAFILE_FILING_2 = "tests/unit/reports/data/legacy-filing.pdf"
TEST_FILENAME = "updated_name.pdf"
PARAM_TEST_FILENAME = "?consumerFilename=updated_name.pdf"
MOCK_AUTH_URL = "https://test.api.connect.gov.bc.ca/mockTarget/auth/api/v1/"
MEDIA_PDF = model_utils.CONTENT_TYPE_PDF
PARAMS1 = (
"?consumerFilename=test.pdf&consumerFilingDate=2005-10-31"
)
PARAMS1 = "?consumerFilename=test.pdf&consumerFilingDate=2005-10-31"
PARAMS2 = "?consumerFilename=test2.pdf&consumerFilingDate=2005-10-31"
PATH: str = "/api/v1/application-reports/{entity_id}/{event_id}/{report_type}"
CHANGE_PATH = "/api/v1/application-reports/{doc_service_id}"
EVENT_PATH = "/api/v1/application-reports/events/{event_id}"
Expand Down Expand Up @@ -116,6 +118,12 @@
("Invalid role", "UT-123456", "123456", "FILING", HTTPStatus.UNAUTHORIZED, "BUSINESS", PRODUCT_ROLES_COLIN),
("Invalid product code", "UT-123456", "123456", "FILING", HTTPStatus.BAD_REQUEST, "XXX", PRODUCT_ROLES_SYSTEM),
]
# testdata pattern is ({description}, {entity_id}, {event_id}, {rtype}, {rtype2}, {status}, {prod_code}, {roles})
TEST_CREATE_DATA_PRODUCT_ADDITIONAL = [
("Valid minimal SA", "UT-123456", "123456", model_utils.REPORT_TYPE_FILING, model_utils.REPORT_TYPE_FILING_2,
HTTPStatus.CREATED, "BUSINESS", PRODUCT_ROLES_SYSTEM),
]

# testdata pattern is ({description}, {entity_id}, {event_id}, {rtype}, {status}, {payload})
TEST_PATCH_DATA = [
("Invalid name", "UT-123456", "123456", "FILING", HTTPStatus.BAD_REQUEST, PATCH_PAYLOAD1),
Expand Down Expand Up @@ -188,6 +196,33 @@
("Conversion AR legacy", "UT9900003", 99900003, "FILING", CC_FILING_AR_LEGACY_INFILE, CC_FILING_AR_LEGACY_OUTFILE,
"UT9900003-CONVL-FILING.pdf"),
]
# testdata pattern is ({description}, {report_type}, {certified_requested}, {certified_allowed})
TEST_CERTIFIED_COPY_REQUEST_DATA = [
("NOA not certified", model_utils.REPORT_TYPE_NOA, False, False),
("NOA certified", model_utils.REPORT_TYPE_NOA, True, True),
("FILING not certified", model_utils.REPORT_TYPE_FILING, False, False),
("FILING certified", model_utils.REPORT_TYPE_FILING, True, True),
("RECEIPT not certified", model_utils.REPORT_TYPE_RECEIPT, False, False),
("RECEIPT certified", model_utils.REPORT_TYPE_RECEIPT, True, False),
("CERT not certified", model_utils.REPORT_TYPE_CERT, False, False),
("CERT certified", model_utils.REPORT_TYPE_CERT, True, False),
("FILING additional 2 not certified", model_utils.REPORT_TYPE_FILING_2, False, False),
("FILING additional 2 certified", model_utils.REPORT_TYPE_FILING_2, True, True),
("FILING additional 3 not certified", model_utils.REPORT_TYPE_FILING_3, False, False),
("FILING additional 3 certified", model_utils.REPORT_TYPE_FILING_3, True, True),
("FILING additional 4 not certified", model_utils.REPORT_TYPE_FILING_4, False, False),
("FILING additional 4 certified", model_utils.REPORT_TYPE_FILING_4, True, True),
]
# testdata pattern is ({description}, {entity_id}, {event_id}, {rtype}, {status}, {prod_code}, {roles})
TEST_PUT_DATA_PRODUCT = [
("Invalid entity ID", "1", "12345", "FILING", HTTPStatus.BAD_REQUEST, "BUSINESS", PRODUCT_ROLES_SYSTEM),
("Invalid event ID", "UT-123456", "JUNK", "FILING", HTTPStatus.BAD_REQUEST, "BUSINESS", PRODUCT_ROLES_SYSTEM),
("Invalid report type", "UT-123456", "123456", "F", HTTPStatus.BAD_REQUEST, "BUSINESS", PRODUCT_ROLES_SYSTEM),
("Invalid no payload", "UT-123456", "123456", "FILING", HTTPStatus.BAD_REQUEST, "BUSINESS", PRODUCT_ROLES_SYSTEM),
("Invalid role", "UT-123456", "123456", "FILING", HTTPStatus.UNAUTHORIZED, "BUSINESS", PRODUCT_ROLES_COLIN),
("Invalid product code", "UT-123456", "123456", "FILING", HTTPStatus.BAD_REQUEST, "XXX", PRODUCT_ROLES_SYSTEM),
("Valid SA", "UT-123456", "123456", "FILING", HTTPStatus.CREATED, "BUSINESS", PRODUCT_ROLES_SYSTEM),
]


@pytest.mark.parametrize("desc,entity_id,event_id,report_type,status,prod_code,roles", TEST_CREATE_DATA_PRODUCT)
Expand Down Expand Up @@ -223,6 +258,106 @@ def test_product_create(session, client, jwt, desc, entity_id, event_id, report_
assert app_report


@pytest.mark.parametrize("desc,entity_id,event_id,report_type,report_type2,status,prod_code,roles", TEST_CREATE_DATA_PRODUCT_ADDITIONAL)
def test_product_create_additional(session, client, jwt, desc, entity_id, event_id, report_type, report_type2, status, prod_code, roles):
"""Assert that a post save new product filing and additional filing reports works as expected."""
if is_ci_testing():
return

# setup
current_app.config.update(AUTH_SVC_URL=MOCK_AUTH_URL)
headers = create_header_account_upload(jwt, roles, "UT-TEST", "PS12345", MEDIA_PDF)
req_path = PATH_PRODUCT.format(prod_code=prod_code, entity_id=entity_id, event_id=event_id, report_type=report_type)
# test
if status != HTTPStatus.CREATED:
response = client.post(req_path, data=None, headers=headers, content_type=MEDIA_PDF)
else:
raw_data = None
req_path += PARAMS1
with open(TEST_DATAFILE_FILING, "rb") as data_file:
raw_data = data_file.read()
data_file.close()
response = client.post(req_path, data=raw_data, headers=headers, content_type=MEDIA_PDF)

# check
assert response.status_code == status
if response.status_code == HTTPStatus.CREATED:
report_json = response.json
assert report_json
assert report_json.get("identifier")
assert report_json.get("url")
assert report_json.get("productCode") == prod_code
assert report_json.get("entityIdentifier") == entity_id
assert report_json.get("eventIdentifier") == int(event_id)
assert report_json.get("reportType") == report_type
app_report: ApplicationReport = ApplicationReport.find_by_doc_service_id(report_json.get("identifier"))
assert app_report
if status == HTTPStatus.CREATED and report_type2:
req_path = PATH_PRODUCT.format(prod_code=prod_code, entity_id=entity_id, event_id=event_id, report_type=report_type2)
req_path += PARAMS2
with open(TEST_DATAFILE_FILING_2, "rb") as data_file:
raw_data = data_file.read()
data_file.close()
response = client.post(req_path, data=raw_data, headers=headers, content_type=MEDIA_PDF)
assert response.status_code == status
report_json = response.json
assert report_json
assert report_json.get("identifier")
assert report_json.get("url")
assert report_json.get("productCode") == prod_code
assert report_json.get("entityIdentifier") == entity_id
assert report_json.get("eventIdentifier") == int(event_id)
assert report_json.get("reportType") == report_type2
app_report: ApplicationReport = ApplicationReport.find_by_doc_service_id(report_json.get("identifier"))
assert app_report


@pytest.mark.parametrize("desc,entity_id,event_id,report_type,status,prod_code,roles", TEST_PUT_DATA_PRODUCT)
def test_product_replace(session, client, jwt, desc, entity_id, event_id, report_type, status, prod_code, roles):
"""Assert that a put replace product report works as expected."""
if is_ci_testing():
return
# setup
current_app.config.update(AUTH_SVC_URL=MOCK_AUTH_URL)
headers = create_header_account_upload(jwt, roles, "UT-TEST", "PS12345", MEDIA_PDF)
req_path = PATH_PRODUCT.format(prod_code=prod_code, entity_id=entity_id, event_id=event_id, report_type=report_type)
# test
if status != HTTPStatus.CREATED:
response = client.post(req_path, data=None, headers=headers, content_type=MEDIA_PDF)
else:
req_path += PARAMS1
raw_data = None
with open(TEST_DATAFILE, "rb") as data_file:
raw_data = data_file.read()
data_file.close()
response = client.post(req_path, data=raw_data, headers=headers, content_type=MEDIA_PDF)
# logger.info(response.json)

# check
assert response.status_code == status
if response.status_code == HTTPStatus.CREATED:
report_json = response.json
assert report_json
assert report_json.get("identifier")
drs_id: str = report_json.get("identifier")
req_path = CHANGE_PATH_PRODUCT.format(prod_code=prod_code, doc_service_id=drs_id)
raw_data = None
with open(TEST_DATAFILE_FILING, "rb") as data_file:
raw_data = data_file.read()
data_file.close()
response2 = client.put(req_path, data=raw_data, headers=headers, content_type=MEDIA_PDF)
# logger.info(response2.json)
assert response2.status_code == status
report2_json = response.json
assert report2_json
assert report2_json.get("identifier") == drs_id
assert report2_json.get("url")
assert report2_json.get("productCode") == prod_code
assert report2_json.get("entityIdentifier") == entity_id
assert report2_json.get("eventIdentifier") == int(event_id)
assert report2_json.get("reportType") == report_type


@pytest.mark.parametrize("desc,entity_id,event_id,report_type,status,payload,prod_code,roles", TEST_PATCH_DATA_PRODUCT)
def test_product_update(session, client, jwt, desc, entity_id, event_id, report_type, status, payload, prod_code, roles):
"""Assert that a request to update report information (not the report itself) works as expected."""
Expand Down Expand Up @@ -575,6 +710,14 @@ def test_get_certified_copy(session, client, jwt, desc, entity_id, event_id, rep
pdf_file.close()


@pytest.mark.parametrize("desc,report_type,certified_requested,certified_allowed", TEST_CERTIFIED_COPY_REQUEST_DATA)
def test_certified_copy(session, client, jwt, desc, report_type, certified_requested, certified_allowed):
"""Assert that the certified copy check for a report type works as expected."""
app_report: ApplicationReport = ApplicationReport(report_type=report_type)
result: bool = is_certified_copy_request(app_report, certified_requested)
assert result == certified_allowed


def is_ci_testing() -> bool:
"""Check unit test environment: exclude pub/sub for CI testing."""
return current_app.config.get("DEPLOYMENT_ENV", "testing") == "testing"
56 changes: 54 additions & 2 deletions document-service/docs/doc-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ paths:
summary: Upload a report response.
value:
identifier: DSR0100001003
entityIdentifier: BC794361
entityIdentifier: BC0794361
eventIdentifier: 4194022
reportType: RECEIPT
dateCreated: '2025-03-03T22:58:45+00:00'
Expand Down Expand Up @@ -274,7 +274,7 @@ paths:
summary: Update a receipt report response.
value:
identifier: DSR0100001003
entityIdentifier: BC794361
entityIdentifier: BC0794361
eventIdentifier: 4194022
reportType: RECEIPT
dateCreated: '2025-03-03T22:58:45+00:00'
Expand All @@ -289,6 +289,58 @@ paths:
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'

put:
tags:
- app-reports
summary: Replace an existing application report.
description: <p>Replace the existing application report record specified by the identifer with the payload document. The identifier is the DRS unique identifier for the report returned in a previous POST or GET response.</p>
operationId: put-product-report
requestBody:
content:
application/pdf:
schema:
type: string
format: binary
responses:
'201':
description: Created
headers:
Access-Control-Allow-Origin:
$ref: '#/components/headers/AccessControlAllowOrigin'
Access-Control-Allow-Methods:
$ref: '#/components/headers/AccessControlAllowMethods'
Access-Control-Allow-Headers:
$ref: '#/components/headers/AccessControlAllowHeaders'
Access-Control-Max-Age:
$ref: '#/components/headers/AccessControlMaxAge'
content:
application/json:
schema:
$ref: '#/components/schemas/applicationReport'
examples:
/api/v1/application-reports/BUSINESS/DSR0100001003/put:
summary: Replace a filing report response.
value:
identifier: DSR0100001003
entityIdentifier: BC0794361
eventIdentifier: 4194022
reportType: FILING
dateCreated: '2025-03-03T22:58:45+00:00'
datePublished: '2023-12-22T16:22:32+00:00'
name: AR-Filing-4194022.pdf
url: 'storage-url-here'
productCode: BUSINESS
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/InternalServerError'


get:
parameters:
- name: certifiedCopy
Expand Down
Loading