Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notice of consideration - part 1 #573

Merged
merged 3 commits into from
Feb 26, 2025
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
37 changes: 37 additions & 0 deletions strr-api/migrations/versions/20250220_0539_6c73c3e3fb36_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""empty message

Revision ID: 6c73c3e3fb36
Revises: 842bb132abc7
Create Date: 2025-02-20 05:39:14.593139

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '6c73c3e3fb36'
down_revision = '7e9fccbeb3ed'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notice_of_consideration',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('application_id', sa.Integer(), nullable=False),
sa.Column('content', sa.Text(), nullable=False),
sa.Column('start_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('end_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('creation_date', sa.DateTime(), server_default=sa.text('(NOW())'), nullable=False),
sa.ForeignKeyConstraint(['application_id'], ['application.id'], name=op.f('fk_notice_of_consideration_application_id_application')),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notice_of_consideration')
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion strr-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "strr-api"
version = "0.0.46"
version = "0.0.47"
description = ""
authors = ["thorwolpert <[email protected]>"]
license = "BSD 3-Clause"
Expand Down
1 change: 1 addition & 0 deletions strr-api/src/strr_api/enums/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class ErrorMessage(Enum):
ADDRESS_LOOK_UP_FAILED = "Failed to look up the address."
APPLICATION_CANNOT_BE_DELETED = "Application in the current status cannot be deleted."
STRR_REQUIREMENTS_FETCH_ERROR = "Unable to retrieve the short term rental requirements for the address."
INVALID_NOC_CONTENT = "Invalid content."


class ApplicationRole(Enum):
Expand Down
2 changes: 2 additions & 0 deletions strr-api/src/strr_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
from .db import db # noqa: I001
from .events import Events
from .ltsa import LTSARecord
from .notice_of_consideration import NoticeOfConsideration
from .platforms import Platform, PlatformBrand, PlatformRegistration, PlatformRepresentative
from .real_time_validation import RealTimeValidation
from .rental import Document, PropertyContact, PropertyListing, PropertyManager, Registration, RentalProperty
Expand All @@ -66,6 +67,7 @@
"AutoApprovalRecord",
"LTSARecord",
"Certificate",
"NoticeOfConsideration",
"Platform",
"PlatformBrand",
"PlatformRegistration",
Expand Down
11 changes: 11 additions & 0 deletions strr-api/src/strr_api/models/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ class Status(BaseEnum):
FULL_REVIEW = auto() # pylint: disable=invalid-name
DECLINED = auto() # pylint: disable=invalid-name
PROVISIONAL = auto() # pylint: disable=invalid-name
NOC_PENDING = auto() # pylint: disable=invalid-name
NOC_EXPIRED = auto() # pylint: disable=invalid-name

__tablename__ = "application"

Expand Down Expand Up @@ -124,6 +126,8 @@ class Status(BaseEnum):
foreign_keys=[registration_id],
)

noc = db.relationship("NoticeOfConsideration", back_populates="application", uselist=False)

@classmethod
def find_by_id(cls, application_id: int) -> Application | None:
"""Return the application by id."""
Expand Down Expand Up @@ -219,6 +223,7 @@ class ApplicationSerializer:
Application.Status.PROVISIONAL_REVIEW: "Approved – Provisional",
Application.Status.FULL_REVIEW: "Pending Approval",
Application.Status.DECLINED: "Declined",
Application.Status.NOC_PENDING: "Notice of Consideration - Pending",
}

HOST_ACTIONS = {Application.Status.PAYMENT_DUE: ["SUBMIT_PAYMENT"]}
Expand All @@ -233,11 +238,13 @@ class ApplicationSerializer:
Application.Status.PROVISIONAL_REVIEW: "Provisional Examination",
Application.Status.FULL_REVIEW: "Full Examination",
Application.Status.DECLINED: "Declined",
Application.Status.NOC_PENDING: "Notice of Consideration - Pending",
}

EXAMINER_ACTIONS = {
Application.Status.FULL_REVIEW_APPROVED: [],
Application.Status.FULL_REVIEW: ["APPROVE", "REJECT"],
Application.Status.NOC_PENDING: ["APPROVE", "REJECT"],
}

@staticmethod
Expand Down Expand Up @@ -304,4 +311,8 @@ def to_dict(application: Application) -> dict:
application_dict["header"]["registrationNumber"] = application.registration.registration_number
application_dict["header"]["isCertificateIssued"] = bool(application.registration.certificates)

if application.noc:
application_dict["header"]["nocStartDate"] = application.noc.start_date.strftime("%Y-%m-%d")
application_dict["header"]["nocEndDate"] = application.noc.end_date.strftime("%Y-%m-%d")

return application_dict
25 changes: 25 additions & 0 deletions strr-api/src/strr_api/models/notice_of_consideration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Model to store Notice of consideration
"""
from __future__ import annotations

from sqlalchemy.sql import text

from strr_api.models.base_model import SimpleBaseModel

from .db import db


class NoticeOfConsideration(SimpleBaseModel):
"""Notice of consideration"""

__tablename__ = "notice_of_consideration"

id = db.Column(db.Integer, primary_key=True, autoincrement=True)
application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False)
content = db.Column(db.Text, nullable=False)
start_date = db.Column(db.DateTime(timezone=True), nullable=False)
end_date = db.Column(db.DateTime(timezone=True), nullable=False)
creation_date = db.Column(db.DateTime, nullable=False, server_default=text("(NOW())"))

application = db.relationship("Application", foreign_keys=[application_id], back_populates="noc")
42 changes: 42 additions & 0 deletions strr-api/src/strr_api/resources/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -843,3 +843,45 @@ def get_related_registrations(application_number: str):
return exception_response(validation_exception)
except ExternalServiceException as external_exception:
return exception_response(external_exception)


@bp.route("/<application_number>/notice-of-consideration", methods=("POST",))
@swag_from({"security": [{"Bearer": []}]})
@cross_origin(origin="*")
@jwt.requires_auth
@jwt.has_one_of_roles([Role.STRR_EXAMINER.value, Role.STRR_INVESTIGATOR.value])
def send_notice_of_consideration(application_number: str):
"""
Send a Notice of consideration for the specified application.
---
tags:
- application
parameters:
- in: path
name: application_number
type: string
required: true
description: Application Number
responses:
200:
description:
401:
description:
"""
try:
UserService.get_or_create_user_by_jwt(g.jwt_oidc_token_info)
json_input = request.get_json()
content = json_input.get("content", "").strip()
if not content:
return error_response(
message=ErrorMessage.INVALID_NOC_CONTENT.value,
http_status=HTTPStatus.BAD_REQUEST,
)
application = ApplicationService.get_application(application_number)
if not application:
return error_response(http_status=HTTPStatus.NOT_FOUND, message=ErrorMessage.APPLICATION_NOT_FOUND.value)
application = ApplicationService.send_notice_of_consideration(application, content)
return jsonify(ApplicationService.serialize(application)), HTTPStatus.OK
except Exception:
logger.error("Error in sending NoC: ", exc_info=True)
return error_response(message=ErrorMessage.PROCESSING_ERROR.value, http_status=HTTPStatus.INTERNAL_SERVER_ERROR)
22 changes: 20 additions & 2 deletions strr-api/src/strr_api/services/application_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Service to interact with the applications model."""
from datetime import datetime, timezone
from datetime import datetime, time, timedelta, timezone
from typing import Optional

import pytz

from strr_api.enums.enum import ApplicationType, PaymentStatus
from strr_api.models import Application, Events, Registration, User
from strr_api.models import Application, Events, NoticeOfConsideration, Registration, User
from strr_api.models.application import ApplicationSerializer
from strr_api.models.dataclass import ApplicationSearch
from strr_api.models.rental import PropertyContact
Expand Down Expand Up @@ -266,3 +268,19 @@
return 0

return RegistrationService.find_all_by_host_sin(host_sin, True)

@staticmethod
def send_notice_of_consideration(application: Application, content: str) -> Application:
"""Sends the notice of consideration."""
notice_of_consideration = NoticeOfConsideration()
notice_of_consideration.content = content
notice_of_consideration.application_id = application.id
notice_of_consideration.start_date = datetime.combine(
datetime.now(pytz.timezone("America/Vancouver")) + timedelta(days=1), time(0, 1, 0)
)
notice_of_consideration.end_date = notice_of_consideration.start_date + timedelta(days=8)
notice_of_consideration.save()
application.status = Application.Status.NOC_PENDING
application.save()
EmailService.send_notice_of_consideration_for_application(application)
return application
18 changes: 18 additions & 0 deletions strr-api/src/strr_api/services/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,21 @@ def send_application_status_update_email(application: Application):
)
except Exception as err:
logger.error("Failed to publish email notification: %s", err.with_traceback(None))

@staticmethod
def send_notice_of_consideration_for_application(application: Application):
"""Send notice of consideration for the application."""
try:
gcp_queue_publisher.publish_to_queue(
gcp_queue_publisher.QueueMessage(
source=EMAIL_SOURCE,
message_type=EMAIL_TYPE,
payload={
"applicationNumber": application.application_number,
"emailType": "NOC",
},
topic=current_app.config.get("GCP_EMAIL_TOPIC"),
)
)
except Exception as err:
logger.error("Failed to publish email notification: %s", err.with_traceback(None))
43 changes: 40 additions & 3 deletions strr-api/tests/postman/strr-api.postman_collection.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
{
"info": {
"_postman_id": "c3eced10-1e79-4b42-9195-c5f5a96088db",
"_postman_id": "e1efdb4c-c185-4c29-95d3-15f2ded359cd",
"name": "strr-api",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "6835935",
"_collection_link": "https://warped-escape-616276.postman.co/workspace/bc-registries~8ef8e652-492a-4d19-b978-d4f0da255b2c/collection/6835935-c3eced10-1e79-4b42-9195-c5f5a96088db?action=share&source=collection_link&creator=6835935"
"_exporter_id": "31792407"
},
"item": [
{
Expand Down Expand Up @@ -1211,6 +1210,44 @@
}
},
"response": []
},
{
"name": "Notice of Consideration",
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{staff_token}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"content\": \"Test NOC\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{api_url}}/applications/{{application_number}}/notice-of-consideration",
"host": [
"{{api_url}}"
],
"path": [
"applications",
"{{application_number}}",
"notice-of-consideration"
]
}
},
"response": []
}
]
},
Expand Down
35 changes: 35 additions & 0 deletions strr-api/tests/unit/resources/test_registration_applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,3 +702,38 @@ def test_delete_payment_pending_applications(session, client, jwt):
print(response_json)
assert HTTPStatus.BAD_REQUEST == rv.status_code
assert response_json["message"] == "Application in the current status cannot be deleted."


@patch("strr_api.services.strr_pay.create_invoice")
@patch("strr_api.services.email_service.EmailService.send_notice_of_consideration_for_application")
def test_examiner_send_notice_of_consideration(mock_noc, mock_invoice, session, client, jwt):
with open(CREATE_HOST_REGISTRATION_REQUEST) as f:
mock_invoice.return_value = MOCK_INVOICE_RESPONSE
mock_noc.return_value = {}
headers = create_header(jwt, [PUBLIC_USER], "Account-Id")
headers["Account-Id"] = ACCOUNT_ID
json_data = json.load(f)
rv = client.post("/applications", json=json_data, headers=headers)
response_json = rv.json
application_number = response_json.get("header").get("applicationNumber")

application = Application.find_by_application_number(application_number=application_number)
application.payment_status = PaymentStatus.COMPLETED.value
application.save()

staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id")
status_update_request = {"content": "Test"}
rv = client.post(
f"/applications/{application_number}/notice-of-consideration",
json=status_update_request,
headers=staff_headers,
)
assert HTTPStatus.OK == rv.status_code
response_json = rv.json
assert response_json.get("header").get("status") == Application.Status.NOC_PENDING
assert response_json.get("header").get("hostStatus") == "Notice of Consideration - Pending"
assert response_json.get("header").get("examinerStatus") == "Notice of Consideration - Pending"
assert response_json.get("header").get("examinerActions") == ["APPROVE", "REJECT"]
assert response_json.get("header").get("hostActions") == []
assert response_json.get("header").get("nocStartDate") is not None
assert response_json.get("header").get("nocEndDate") is not None
Loading