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 legal-api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "legal-api"
version = "3.1.24"
version = "3.1.25"
description = ""
authors = [
{name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from flask_cors import cross_origin

from business_model.models import Business, CourtOrder, Filing
from business_model.models.types.filings import FilingTypes
from legal_api.services import authorized
from legal_api.utils.auth import jwt

Expand Down Expand Up @@ -47,24 +48,28 @@ def get_court_orders(identifier, court_order_id=None):
return jsonify(court_order), code

court_orders_list = CourtOrder.get_json_with_filing_type(business.id)
for court_order in court_orders_list:
_include_court_order_files(court_order, business)
return jsonify({
"courtOrders": court_orders_list
}), HTTPStatus.OK


def _get_court_order(business, court_order_id=None):
def _get_court_order(business, court_order_id):
if court_order := CourtOrder.get_by_id(court_order_id):
court_order_json = court_order.json
filing = Filing.find_by_id(court_order.filing_id)
if filing.filing_type == "courtOrder":
_include_court_order_files(court_order_json, filing, business)
_include_court_order_files(court_order_json, business)

return {"courtOrder": court_order_json}, HTTPStatus.OK

return {"message": f"{business.identifier} court order not found"}, HTTPStatus.NOT_FOUND


def _include_court_order_files(court_order_json, filing, business):
def _include_court_order_files(court_order_json, business):
filing = Filing.find_by_id(court_order_json["filingId"])
if filing.filing_type != FilingTypes.COURTORDER:
return

if documents := filing.documents.all():
base_url = current_app.config.get("BUSINESS_API_GW_URL")
doc_url = url_for(
Expand Down
115 changes: 94 additions & 21 deletions legal-api/src/legal_api/services/filings/validations/correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""Validation for the Correction filing."""
from datetime import timedelta
from datetime import UTC, datetime, timedelta
from http import HTTPStatus
from typing import Final

Expand Down Expand Up @@ -45,6 +45,7 @@
validate_continuation_in_expro_business_in_colin,
validate_continuation_in_foreign_jurisdiction,
)
from legal_api.services.filings.validations.dissolution import validate_custodian_email
from legal_api.services.filings.validations.incorporation_application import (
validate_coop_parties_mailing_address,
validate_roles,
Expand Down Expand Up @@ -97,21 +98,6 @@ def validate(business: Business, filing: dict) -> Error:
if not is_comment_only_correction:
if filing.get("filing", {}).get("correction", {}).get("parties", None):
msg.extend(validate_parties_addresses(filing, filing_type))
if filing.get("filing", {}).get("correction", {}).get("relationships", None):
msg.extend(validate_relationships(
business,
filing,
filing_type,
[
PartyRole.RoleTypes.DIRECTOR,
PartyRole.RoleTypes.LIQUIDATOR,
PartyRole.RoleTypes.RECEIVER,
PartyRole.RoleTypes.COMPLETING_PARTY
],
True,
True,
[PartyRole.RoleTypes.DIRECTOR, PartyRole.RoleTypes.COMPLETING_PARTY]
))
if filing.get("filing", {}).get("correction", {}).get("offices", None):
msg.extend(validate_offices_addresses(filing, filing_type))

Expand Down Expand Up @@ -145,6 +131,81 @@ def _validate_firms_correction(business: Business, filing, legal_type, msg):


def _validate_corps_correction(business: Business, filing_dict, legal_type, msg):
if filing_dict.get("filing", {}).get("correction", {}).get("courtOrder", None):
msg.extend(court_order_validation(filing_dict))
msg.extend(_validate_court_orders_correction(filing_dict, business))

if relationships := filing_dict.get("filing", {}).get("correction", {}).get("relationships", None):
relationships_path = "/filing/correction/relationships"
completing_parties = [
x for x in relationships
if any(
role for role in x.get("roles", [])
if role["roleType"].lower().replace(" ", "_") == PartyRole.RoleTypes.COMPLETING_PARTY.value
)
]
correction_type = filing_dict.get("filing").get("correction").get("type", "STAFF")
if correction_type == "STAFF":
if len(completing_parties) != 0:
msg.append({
"error": "Should not provide completing party when correction type is STAFF",
"path": relationships_path
})
elif len(completing_parties) == 0:
msg.append({"error": "Completing party is required.", "path": relationships_path})
elif len(completing_parties) > 1:
msg.append({"error": "Only one completing party is allowed.", "path": relationships_path})

if business.state == Business.State.HISTORICAL.value:
_validate_corps_correction_historical(business, filing_dict, msg)
else:
_validate_corps_correction_active(business, filing_dict, legal_type, msg)


def _validate_corps_correction_historical(business: Business, filing_dict, msg):
filing_type = "correction"
msg.extend(_validate_out_correction(filing_dict, filing_type, business))
if relationships := filing_dict.get("filing", {}).get("correction", {}).get("relationships", None):
custodian_parties = [
x for x in relationships
if any(
role for role in x.get("roles", [])
if role["roleType"].lower() == PartyRole.RoleTypes.CUSTODIAN.value
)
]
relationships_path = "/filing/correction/relationships"
if len(custodian_parties) > 1:
msg.append({"error": "Only one custodian is allowed.", "path": relationships_path})
elif len(custodian_parties) == 1:
today = datetime.now(tz=UTC).date()
existing_custodian = PartyRole.get_party_roles(business.id, today, PartyRole.RoleTypes.CUSTODIAN.value)
if not custodian_parties[0].get("entity", {}).get("identifier") and len(existing_custodian) > 0:
msg.append({
"error": "Custodian already exists for this business, cannot create another custodian.",
"path": relationships_path
})
msg.extend(
validate_custodian_email(
custodian_parties[0].get("entity", {}).get("email"),
f"{relationships_path}/entity/email"
)
)

msg.extend(validate_relationships(
business,
filing_dict,
filing_type,
[
PartyRole.RoleTypes.CUSTODIAN,
PartyRole.RoleTypes.COMPLETING_PARTY
],
True,
True,
[PartyRole.RoleTypes.CUSTODIAN, PartyRole.RoleTypes.COMPLETING_PARTY]
))


def _validate_corps_correction_active(business: Business, filing_dict, legal_type, msg):
filing_type = "correction"
if new_legal_type := filing_dict.get("filing", {}).get("correction", {}).get("newLegalType"):
if business.legal_type == new_legal_type:
Expand All @@ -163,6 +224,23 @@ def _validate_corps_correction(business: Business, filing_dict, legal_type, msg)
msg.extend(err)
# FUTURE: this should be removed when COLIN sync back is no longer required.
msg.extend(validate_parties_names(filing_dict, filing_type, legal_type))

if filing_dict.get("filing", {}).get("correction", {}).get("relationships", None):
msg.extend(validate_relationships(
business,
filing_dict,
filing_type,
[
PartyRole.RoleTypes.DIRECTOR,
PartyRole.RoleTypes.LIQUIDATOR,
PartyRole.RoleTypes.RECEIVER,
PartyRole.RoleTypes.COMPLETING_PARTY
],
True,
True,
[PartyRole.RoleTypes.DIRECTOR, PartyRole.RoleTypes.COMPLETING_PARTY]
))

if filing_dict.get("filing", {}).get("correction", {}).get("shareStructure", None):
err = validate_share_structure(filing_dict, filing_type, legal_type)
if err:
Expand All @@ -171,13 +249,8 @@ def _validate_corps_correction(business: Business, filing_dict, legal_type, msg)
msg.extend(validate_share_currency(filing_dict, filing_type, business))
msg.extend(validate_resolution_date_in_share_structure(filing_dict, filing_type, business))

if filing_dict.get("filing", {}).get("correction", {}).get("courtOrder", None):
msg.extend(court_order_validation(filing_dict))

msg.extend(_validate_continuation_in_correction(filing_dict, filing_type, legal_type, business))
msg.extend(_validate_out_correction(filing_dict, filing_type, business))
msg.extend(_validate_amalgamation_correction(filing_dict, filing_type, business))
msg.extend(_validate_court_orders_correction(filing_dict, business))


def _validate_court_orders_correction(filing_dict, business: Business):
Expand Down
123 changes: 55 additions & 68 deletions legal-api/src/legal_api/services/filings/validations/dissolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ def validate(business: Business, dissolution: dict) -> Error | None:
if err:
msg.extend(err)

# Specific validation for addresses in dissolution
err = validate_dissolution_parties_address(dissolution, business.legal_type, dissolution_type)
if err:
msg.extend(err)
msg.extend(validate_dissolution_parties_address(dissolution, business.legal_type, dissolution_type))

if dissolution["filing"]["dissolution"].get("parties"):
# Common validation for addresses
Expand Down Expand Up @@ -253,31 +250,39 @@ def validate_dissolution_parties_address(filing_json, legal_type, dissolution_ty
This needs not to be validated for SP and GP
This needs not to be validated for administrative dissolution
"""
if dissolution_type in [DissolutionTypes.ADMINISTRATIVE, DissolutionTypes.DELAY]:
return None
msg = []
if (
dissolution_type in [DissolutionTypes.ADMINISTRATIVE, DissolutionTypes.DELAY] or
legal_type in [Business.LegalTypes.SOLE_PROP.value, Business.LegalTypes.PARTNERSHIP.value] or
"parties" not in filing_json["filing"]["dissolution"]
):
return msg

if legal_type in [Business.LegalTypes.SOLE_PROP.value, Business.LegalTypes.PARTNERSHIP.value]:
return None
parties_json = filing_json["filing"]["dissolution"]["parties"]
custodian_count = 0

if "parties" not in filing_json["filing"]["dissolution"]:
return None
for idx, party in enumerate(parties_json):
if _is_custodian_role(party.get("roles", [])):
custodian_count += 1
path = f"/filing/dissolution/parties/{idx}"
if legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value:
# only validate email for CORP voluntary dissolution
email = party.get("officer", {}).get("email")
msg.extend(validate_custodian_email(email, f"{path}/officer/email"))
msg.extend(_validate_custodian_name(party, path))

parties_json = filing_json["filing"]["dissolution"]["parties"]
custodians = list(filter(lambda x: _is_custodian_role(x.get("roles", [])), parties_json))
for address_type in Address.JSON_ADDRESS_TYPES:
msg.extend(_validate_party_address(party, idx, address_type, legal_type in Business.CORPS))

if not custodians:
if custodian_count == 0:
# Handle case where there are no custodians, but there is a liquidator role
# (this is not implemented in the Create UI, keeping behavior here)
if any(_is_liquidator_role(p.get("roles", [])) for p in parties_json):
return None
return [{"error": "Dissolution party is required.", "path": "/filing/dissolution/parties"}]

msg = []
msg.extend(_validate_custodian_email(custodians, dissolution_type, legal_type))
msg.extend(_validate_custodian_name(custodians, dissolution_type, legal_type))
msg.extend(_validate_address_location(custodians, legal_type))
return msg
msg.append({"error": "Dissolution party is required.", "path": "/filing/dissolution/parties"})
return msg

return msg or None
return msg


def _is_custodian_role(roles: list) -> bool:
Expand All @@ -290,16 +295,6 @@ def _is_liquidator_role(roles: list) -> bool:
for role in roles)


def _validate_address_location(parties, legal_type):
"""Every party address must be in Canada; CORP types also require the BC province."""
msg = []
require_bc = legal_type in Business.CORPS
for idx, party in enumerate(parties):
for address_type in Address.JSON_ADDRESS_TYPES:
msg.extend(_validate_party_address(party, idx, address_type, require_bc))
return msg


def _validate_party_address(party, idx, address_type, require_bc):
address_path = f"/filing/dissolution/parties/{idx}/{address_type}"
if address_type not in party:
Expand Down Expand Up @@ -356,58 +351,50 @@ def _validate_court_order(filing):
return []


def _validate_custodian_email(parties, dissolution_type, legal_type) -> list:
def validate_custodian_email(email, path) -> list:
"""Validate custodian email for voluntary dissolution."""
# Only validate for CORP voluntary dissolution
if not (legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value):
return []

msg = []
for idx, party in enumerate(parties):
email = get_str(party, "/officer/email")
if not email:
msg.append({"error": "Custodian email is required for voluntary dissolution.",
"path": f"/filing/dissolution/parties/{idx}/officer/email"})
elif any(char.isspace() for char in email):
msg.append({
"error": "Custodian email cannot contain any whitespaces.",
"path": f"/filing/dissolution/parties/{idx}/officer/email"
})
if not email:
msg.append({"error": "Custodian email is required.",
"path": path})
elif any(char.isspace() for char in email):
msg.append({
"error": "Custodian email cannot contain any whitespaces.",
"path": path
})
return msg

def _validate_custodian_name(parties, dissolution_type, legal_type) -> list:
"""Validate custodian name of the dissolution filing and trim it."""
# Only validate for CORP voluntary dissolution
if not (legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value):
return []

def _validate_custodian_name(custodian, path) -> list:
"""Validate custodian name of the dissolution filing and trim it."""
msg = []
for idx, party in enumerate(parties):
party_type = get_str(party, "/officer/partyType")
# Organization custodian name (required + no surrounding whitespace) is enforced by the
# schema (business-schemas parties officer.organizationName pattern). firstName is not
# schema-patterned, so it is still validated here.
if party_type != "organization":
first_name = get_str(party, "/officer/firstName")

if first_name is None or not first_name.strip():
msg.append({
"error": "Custodian first name is required.",
"path": f"/filing/dissolution/parties/{idx}/officer/firstName"
party_type = get_str(custodian, "/officer/partyType")
# Organization custodian name (required + no surrounding whitespace) is enforced by the
# schema (business-schemas parties officer.organizationName pattern).
# firstName is not schema-patterned, so it is still validated here.
if party_type != "organization":
first_name = get_str(custodian, "/officer/firstName")

if first_name is None or not first_name.strip():
msg.append({
"error": "Custodian first name is required.",
"path": f"{path}/officer/firstName"
})
elif first_name != first_name.strip():
msg.append({
"error": "Custodian first name cannot have leading or trailing spaces.",
"path": f"/filing/dissolution/parties/{idx}/officer/firstName"
elif first_name != first_name.strip():
msg.append({
"error": "Custodian first name cannot have leading or trailing spaces.",
"path": f"{path}/officer/firstName"
})

return msg


def _check_dissolution_permission(required_permission: str, dissolution_type: str, filing_type: str) -> Error | None:
"""Check if the user has the required permission for the dissolution filing."""
message = "Permission Denied - You do not have permissions file {dissolution_type} {filing_type} filing."
return PermissionService.check_user_permission(required_permission, message=message)


def _validate_dissolution_permission(business: Business, dissolution: dict, dissolution_type: str, filing_type: str, msg: list) -> Error | None:
"""Validate dissolution permission based on business and dissolution type."""

Expand Down Expand Up @@ -449,4 +436,4 @@ def _validate_dissolution_permission(business: Business, dissolution: dict, diss
"check_email":True,
"check_address":True,
"check_document_email":True}
)
)
Loading