From ef7c4a1a8335c26d4962f26725af5077e3b678ad Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Tue, 17 Feb 2026 14:40:51 -0800 Subject: [PATCH 1/4] 31365 ar reminder opt out allowable actions and permissions --- legal-api/manage.py | 20 ---- .../edbd0f6ef10e_ar_reminder_opt_out.py | 98 +++++++++++++++++++ legal-api/pre-hook-update-db.sh | 13 +-- legal-api/pre_hook_create_database.py | 24 ----- legal-api/src/legal_api/services/authz.py | 1 + .../tests/unit/services/test_authorization.py | 4 + .../business-registry-model/pyproject.toml | 2 +- ...22138_edbd0f6ef10e_ar_reminder_opt_out_.py | 96 ++++++++++++++++++ 8 files changed, 202 insertions(+), 56 deletions(-) create mode 100644 legal-api/migrations/versions/edbd0f6ef10e_ar_reminder_opt_out.py delete mode 100644 legal-api/pre_hook_create_database.py create mode 100644 python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py diff --git a/legal-api/manage.py b/legal-api/manage.py index 703a97702a..7ab202b25e 100644 --- a/legal-api/manage.py +++ b/legal-api/manage.py @@ -16,7 +16,6 @@ """ import logging -from flask import url_for from flask_migrate import Migrate, MigrateCommand from flask_script import Manager # class for handling a set of commands @@ -32,25 +31,6 @@ MANAGER.add_command("db", MigrateCommand) - -@MANAGER.command -def list_routes(): - output = [] - for rule in APP.url_map.iter_rules(): - - options = {} - for arg in rule.arguments: - options[arg] = f"[{arg}]" - - methods = ",".join(rule.methods) - url = url_for(rule.endpoint, **options) - line = (f"{rule.endpoint:50s} {methods:20s} {url}") - output.append(line) - - for line in sorted(output): - print(line) # noqa: T201 - - if __name__ == "__main__": logging.log(logging.INFO, "Running the Manager") MANAGER.run() diff --git a/legal-api/migrations/versions/edbd0f6ef10e_ar_reminder_opt_out.py b/legal-api/migrations/versions/edbd0f6ef10e_ar_reminder_opt_out.py new file mode 100644 index 0000000000..0f5df139c0 --- /dev/null +++ b/legal-api/migrations/versions/edbd0f6ef10e_ar_reminder_opt_out.py @@ -0,0 +1,98 @@ +"""ar_reminder_opt_out + +Revision ID: edbd0f6ef10e +Revises: 7efd0c42babd +Create Date: 2026-02-13 23:48:54.926017 + +""" +from datetime import datetime, timezone +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'edbd0f6ef10e' +down_revision = '7efd0c42babd' +branch_labels = None +depends_on = None + + +def upgrade(): + now = datetime.now(timezone.utc) + + permissions = sa.table( + 'permissions', + sa.column('id', sa.Integer), + sa.column('permission_name', sa.String), + sa.column('description', sa.String), + sa.column('created_date', sa.TIMESTAMP(timezone=True)), + sa.column('last_modified', sa.TIMESTAMP(timezone=True)), + sa.column('created_by_id', sa.Integer), + sa.column('modified_by_id', sa.Integer) + ) + + op.bulk_insert( + permissions, + [{ + 'permission_name': 'AR_REMINDER_OPT_OUT', + 'description': 'Authorized to access Opt out ar reminder option.', + 'created_date': now, + 'last_modified': now, + 'created_by_id': None, + 'modified_by_id': None + }] + ) + + bind = op.get_bind() + + roles = bind.execute(sa.text("SELECT id, role_name FROM authorized_roles where role_name in ('sbc_staff', 'staff', 'public_user')")).mappings().all() + role_map = {r['role_name']: r['id'] for r in roles} + + permission = bind.execute( + sa.text("SELECT id FROM permissions WHERE permission_name = 'AR_REMINDER_OPT_OUT'") + ).mappings().first() + permission_id = permission['id'] + + authorized_role_permissions = sa.table( + 'authorized_role_permissions', + sa.column('role_id', sa.Integer), + sa.column('permission_id', sa.Integer), + sa.column('created_date', sa.TIMESTAMP(timezone=True)), + sa.column('last_modified', sa.TIMESTAMP(timezone=True)), + sa.column('created_by_id', sa.Integer), + sa.column('modified_by_id', sa.Integer) + ) + + role_permission_list = [ + { + 'role_id': role_id, + 'permission_id': permission_id, + 'created_date': now, + 'last_modified': now, + 'created_by_id': None, + 'modified_by_id': None + } + for role_id in role_map.values() + ] + + op.bulk_insert(authorized_role_permissions, role_permission_list) + + + +def downgrade(): + bind = op.get_bind() + + permission = bind.execute( + sa.text("SELECT id FROM permissions WHERE permission_name = 'AR_REMINDER_OPT_OUT'") + ).mappings().first() + if permission: + permission_id = permission['id'] + + op.execute( + sa.text("DELETE FROM authorized_role_permissions WHERE permission_id = :pid").bindparams(pid=permission_id) + ) + + op.execute( + sa.text("DELETE FROM permissions WHERE id = :pid").bindparams(pid=permission_id) + ) + diff --git a/legal-api/pre-hook-update-db.sh b/legal-api/pre-hook-update-db.sh index fe6814c43b..3257419cfa 100755 --- a/legal-api/pre-hook-update-db.sh +++ b/legal-api/pre-hook-update-db.sh @@ -1,14 +1,5 @@ #! /bin/sh -#export LIBRARY_PATH=/opt/rh/httpd24/root/usr/lib64 -#export X_SCLS=rh-python35 httpd24 -#export LD_LIBRARY_PATH=/opt/rh/rh-python35/root/usr/lib64:/opt/rh/httpd24/root/usr/lib64 -#export PATH=/opt/app-root/bin:/opt/rh/rh-python35/root/usr/bin:/opt/rh/httpd24/root/usr/bin:/opt/rh/httpd24/root/usr/sbin:/opt/app-root/src/.local/bin/:/opt/app-root/src/bin:/opt/app-root/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin - -# psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'my_db'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE my_db" - cd /opt/app-root -echo 'ensure database is created' -python pre_hook_create_database.py - echo 'starting upgrade' -python manage.py db upgrade +poetry run python manage.py db upgrade +echo 'upgrade completed' diff --git a/legal-api/pre_hook_create_database.py b/legal-api/pre_hook_create_database.py deleted file mode 100644 index bd9b289dad..0000000000 --- a/legal-api/pre_hook_create_database.py +++ /dev/null @@ -1,24 +0,0 @@ -import contextlib -import os -import sys - -import sqlalchemy -import sqlalchemy.exc - -from legal_api.config import ProdConfig - -DB_ADMIN_PASSWORD = os.getenv("DB_ADMIN_PASSWORD", None) -DB_ADMIN_USERNAME = os.getenv("DB_ADMIN_USERNAME", "postgres") - -if not hasattr(ProdConfig, "DB_NAME") or not DB_ADMIN_PASSWORD: - print("Unable to create database.", sys.stdout) # noqa: T201 - sys.exit(-1) - -DATABASE_URI = f"postgresql://{DB_ADMIN_USERNAME}:{DB_ADMIN_PASSWORD}@{ProdConfig.DB_HOST}:{int(ProdConfig.DB_PORT)}/{DB_ADMIN_USERNAME}" - -with contextlib.suppress(sqlalchemy.exc.ProgrammingError), sqlalchemy.create_engine( - DATABASE_URI, - isolation_level="AUTOCOMMIT" -).connect() as connection: - database = ProdConfig.DB_NAME - connection.execute(f"CREATE DATABASE {database}") diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index a03f06789b..8554ac207c 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -757,6 +757,7 @@ def get_allowable_actions(jwt: JwtManager, business: Business): "filingSubmissionLink": filing_submission_url, "filingTypes": allowed_filings }, + "arReminders": (business.legal_type in Business.CORPS and business.state == Business.State.ACTIVE), "digitalBusinessCard": are_digital_credentials_allowed(business, jwt), "digitalBusinessCardPreconditions": get_digital_credentials_preconditions(business), "viewAll": is_competent_authority(jwt) diff --git a/legal-api/tests/unit/services/test_authorization.py b/legal-api/tests/unit/services/test_authorization.py index 5fab81f461..cf4ffefc25 100644 --- a/legal-api/tests/unit/services/test_authorization.py +++ b/legal-api/tests/unit/services/test_authorization.py @@ -1262,6 +1262,10 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me assert result['filing']['filingSubmissionLink'] assert result['filing']['filingTypes'] == expected assert result['viewAll'] == is_comp_auth + if state == Business.State.ACTIVE and legal_type in Business.CORPS: + assert result["arReminders"] == True + else: + assert result["arReminders"] == False @pytest.mark.parametrize( diff --git a/python/common/business-registry-model/pyproject.toml b/python/common/business-registry-model/pyproject.toml index 70d6b4c990..625ac88a8a 100644 --- a/python/common/business-registry-model/pyproject.toml +++ b/python/common/business-registry-model/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-model" -version = "3.3.20" +version = "3.3.21" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py b/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py new file mode 100644 index 0000000000..4aa77c78fb --- /dev/null +++ b/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py @@ -0,0 +1,96 @@ +"""ar_reminder_opt_out + +Revision ID: edbd0f6ef10e +Revises: 7efd0c42babd +Create Date: 2026-02-17 22:21:38.643521 + +""" +from datetime import datetime, timezone +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'edbd0f6ef10e' +down_revision = '7efd0c42babd' +branch_labels = None +depends_on = None + + +def upgrade(): + now = datetime.now(timezone.utc) + + permissions = sa.table( + 'permissions', + sa.column('id', sa.Integer), + sa.column('permission_name', sa.String), + sa.column('description', sa.String), + sa.column('created_date', sa.TIMESTAMP(timezone=True)), + sa.column('last_modified', sa.TIMESTAMP(timezone=True)), + sa.column('created_by_id', sa.Integer), + sa.column('modified_by_id', sa.Integer) + ) + + op.bulk_insert( + permissions, + [{ + 'permission_name': 'AR_REMINDER_OPT_OUT', + 'description': 'Authorized to access Opt out ar reminder option.', + 'created_date': now, + 'last_modified': now, + 'created_by_id': None, + 'modified_by_id': None + }] + ) + + bind = op.get_bind() + + roles = bind.execute(sa.text("SELECT id, role_name FROM authorized_roles where role_name in ('sbc_staff', 'staff', 'public_user')")).mappings().all() + role_map = {r['role_name']: r['id'] for r in roles} + + permission = bind.execute( + sa.text("SELECT id FROM permissions WHERE permission_name = 'AR_REMINDER_OPT_OUT'") + ).mappings().first() + permission_id = permission['id'] + + authorized_role_permissions = sa.table( + 'authorized_role_permissions', + sa.column('role_id', sa.Integer), + sa.column('permission_id', sa.Integer), + sa.column('created_date', sa.TIMESTAMP(timezone=True)), + sa.column('last_modified', sa.TIMESTAMP(timezone=True)), + sa.column('created_by_id', sa.Integer), + sa.column('modified_by_id', sa.Integer) + ) + + role_permission_list = [ + { + 'role_id': role_id, + 'permission_id': permission_id, + 'created_date': now, + 'last_modified': now, + 'created_by_id': None, + 'modified_by_id': None + } + for role_id in role_map.values() + ] + + op.bulk_insert(authorized_role_permissions, role_permission_list) + + +def downgrade(): + bind = op.get_bind() + + permission = bind.execute( + sa.text("SELECT id FROM permissions WHERE permission_name = 'AR_REMINDER_OPT_OUT'") + ).mappings().first() + if permission: + permission_id = permission['id'] + + op.execute( + sa.text("DELETE FROM authorized_role_permissions WHERE permission_id = :pid").bindparams(pid=permission_id) + ) + + op.execute( + sa.text("DELETE FROM permissions WHERE id = :pid").bindparams(pid=permission_id) + ) From cf46e19aa3421acb6ed62559b4647ae9aa2f4acc Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Tue, 17 Feb 2026 14:50:55 -0800 Subject: [PATCH 2/4] no message --- .../20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py b/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py index 4aa77c78fb..1db253687b 100644 --- a/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py +++ b/python/common/business-registry-model/src/business_model_migrations/versions/20260217_222138_edbd0f6ef10e_ar_reminder_opt_out_.py @@ -5,10 +5,10 @@ Create Date: 2026-02-17 22:21:38.643521 """ -from datetime import datetime, timezone -from alembic import op -import sqlalchemy as sa +from datetime import UTC, datetime, timezone +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision = 'edbd0f6ef10e' @@ -18,7 +18,7 @@ def upgrade(): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) permissions = sa.table( 'permissions', From eef470d8d0c4750ab9ca84fd67a50c5d30e6ed7c Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Tue, 17 Feb 2026 15:50:29 -0800 Subject: [PATCH 3/4] no message --- legal-api/src/legal_api/services/authz.py | 4 +++- legal-api/tests/unit/services/test_authorization.py | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index 8554ac207c..95bdba33f8 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -757,11 +757,13 @@ def get_allowable_actions(jwt: JwtManager, business: Business): "filingSubmissionLink": filing_submission_url, "filingTypes": allowed_filings }, - "arReminders": (business.legal_type in Business.CORPS and business.state == Business.State.ACTIVE), "digitalBusinessCard": are_digital_credentials_allowed(business, jwt), "digitalBusinessCardPreconditions": get_digital_credentials_preconditions(business), "viewAll": is_competent_authority(jwt) } + if business.legal_type in Business.CORPS: + result["arReminder"] = (business.state == Business.State.ACTIVE) + return result diff --git a/legal-api/tests/unit/services/test_authorization.py b/legal-api/tests/unit/services/test_authorization.py index cf4ffefc25..573b37af1e 100644 --- a/legal-api/tests/unit/services/test_authorization.py +++ b/legal-api/tests/unit/services/test_authorization.py @@ -1262,10 +1262,14 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me assert result['filing']['filingSubmissionLink'] assert result['filing']['filingTypes'] == expected assert result['viewAll'] == is_comp_auth - if state == Business.State.ACTIVE and legal_type in Business.CORPS: - assert result["arReminders"] == True + if legal_type in Business.CORPS: + if state == Business.State.ACTIVE: + assert result["arReminder"] == True + else: + assert result["arReminder"] == False else: - assert result["arReminders"] == False + assert "arReminder" not in result + @pytest.mark.parametrize( From 2f1d2bf380b9c12c43433ce78bd072da5a9c7c03 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Wed, 18 Feb 2026 10:09:22 -0800 Subject: [PATCH 4/4] no message --- legal-api/pre-hook-update-db.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/legal-api/pre-hook-update-db.sh b/legal-api/pre-hook-update-db.sh index 3257419cfa..04f190e701 100755 --- a/legal-api/pre-hook-update-db.sh +++ b/legal-api/pre-hook-update-db.sh @@ -1,5 +1,4 @@ #! /bin/sh -cd /opt/app-root echo 'starting upgrade' poetry run python manage.py db upgrade echo 'upgrade completed'