"""services phase 4A due date engine foundation Revision ID: 20260504_due_date_engine_foundation Revises: 20260503_assurance_review_partner Create Date: 2026-05-04 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "20260504_due_date_engine_foundation" down_revision: Union[str, None] = "20260503_assurance_review_partner" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def _insp(): return sa.inspect(op.get_bind()) def _has_table(table_name: str) -> bool: return table_name in _insp().get_table_names() def _has_column(table_name: str, column_name: str) -> bool: if not _has_table(table_name): return False return any(c["name"] == column_name for c in _insp().get_columns(table_name)) def _has_index(table_name: str, index_name: str) -> bool: if not _has_table(table_name): return False return any(ix.get("name") == index_name for ix in _insp().get_indexes(table_name)) def _create_index_if_missing(table_name: str, index_name: str, columns: list[str]) -> None: if _has_table(table_name) and not _has_index(table_name, index_name): op.create_index(index_name, table_name, columns) def upgrade() -> None: if not _has_table("service_due_date_rules"): op.create_table( "service_due_date_rules", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("service_catalogue_id", sa.Integer(), nullable=False), sa.Column("rule_name", sa.String(length=150), nullable=False), sa.Column("period_type", sa.String(length=30), nullable=False, server_default="yearly"), sa.Column("due_year_basis", sa.String(length=40), nullable=False, server_default="assessment_year_start"), sa.Column("due_day", sa.Integer(), nullable=True), sa.Column("due_month", sa.Integer(), nullable=True), sa.Column("due_month_offset", sa.Integer(), nullable=False, server_default="0"), sa.Column("days_offset_after_event", sa.Integer(), nullable=True), sa.Column("sort_order", sa.Integer(), nullable=False, server_default="100"), sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("remarks", sa.Text(), nullable=True), sa.Column("created_by_user_id", sa.Integer(), nullable=True), sa.Column("updated_by_user_id", sa.Integer(), nullable=True), sa.Column("created_at_utc", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at_utc", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.ForeignKeyConstraint(["service_catalogue_id"], ["service_catalogues.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["created_by_user_id"], ["users.id"]), sa.ForeignKeyConstraint(["updated_by_user_id"], ["users.id"]), sa.UniqueConstraint("service_catalogue_id", "rule_name", name="uq_service_due_date_rules_catalogue_name"), ) _create_index_if_missing("service_due_date_rules", "ix_service_due_date_rules_service_catalogue_id", ["service_catalogue_id"]) _create_index_if_missing("service_due_date_rules", "ix_service_due_date_rules_period_type", ["period_type"]) _create_index_if_missing("service_due_date_rules", "ix_service_due_date_rules_is_active", ["is_active"]) if not _has_table("service_due_date_extensions"): op.create_table( "service_due_date_extensions", sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True), sa.Column("tenant_id", sa.Integer(), nullable=False), sa.Column("service_catalogue_id", sa.Integer(), nullable=False), sa.Column("due_date_rule_id", sa.Integer(), nullable=True), sa.Column("financial_year", sa.String(length=9), nullable=False), sa.Column("assessment_year", sa.String(length=9), nullable=True), sa.Column("period_label", sa.String(length=30), nullable=True), sa.Column("previous_due_date", sa.Date(), nullable=True), sa.Column("extended_due_date", sa.Date(), nullable=False), sa.Column("extension_sequence", sa.Integer(), nullable=False, server_default="1"), sa.Column("notification_reference", sa.String(length=200), nullable=True), sa.Column("notification_date", sa.Date(), nullable=True), sa.Column("remarks", sa.Text(), nullable=True), sa.Column("created_by_user_id", sa.Integer(), nullable=True), sa.Column("updated_by_user_id", sa.Integer(), nullable=True), sa.Column("created_at_utc", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at_utc", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.ForeignKeyConstraint(["tenant_id"], ["tenants.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["service_catalogue_id"], ["service_catalogues.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["due_date_rule_id"], ["service_due_date_rules.id"], ondelete="SET NULL"), sa.ForeignKeyConstraint(["created_by_user_id"], ["users.id"]), sa.ForeignKeyConstraint(["updated_by_user_id"], ["users.id"]), ) for index_name, columns in [ ("ix_service_due_date_extensions_tenant_id", ["tenant_id"]), ("ix_service_due_date_extensions_service_catalogue_id", ["service_catalogue_id"]), ("ix_service_due_date_extensions_due_date_rule_id", ["due_date_rule_id"]), ("ix_service_due_date_extensions_financial_year", ["financial_year"]), ("ix_service_due_date_extensions_assessment_year", ["assessment_year"]), ("ix_service_due_date_extensions_period_label", ["period_label"]), ("ix_service_due_date_extensions_extended_due_date", ["extended_due_date"]), ]: _create_index_if_missing("service_due_date_extensions", index_name, columns) subscription_columns = [ ("due_date_rule_id", sa.Column("due_date_rule_id", sa.Integer(), nullable=True)), ("original_due_date", sa.Column("original_due_date", sa.Date(), nullable=True)), ("current_due_date", sa.Column("current_due_date", sa.Date(), nullable=True)), ("due_date_source", sa.Column("due_date_source", sa.String(length=30), nullable=True)), ] for column_name, column in subscription_columns: if not _has_column("client_service_subscriptions", column_name): op.add_column("client_service_subscriptions", column) for index_name, columns in [ ("ix_client_service_subscriptions_due_date_rule_id", ["due_date_rule_id"]), ("ix_client_service_subscriptions_original_due_date", ["original_due_date"]), ("ix_client_service_subscriptions_current_due_date", ["current_due_date"]), ]: _create_index_if_missing("client_service_subscriptions", index_name, columns) def downgrade() -> None: for index_name in [ "ix_client_service_subscriptions_current_due_date", "ix_client_service_subscriptions_original_due_date", "ix_client_service_subscriptions_due_date_rule_id", ]: if _has_index("client_service_subscriptions", index_name): op.drop_index(index_name, table_name="client_service_subscriptions") for column_name in ["due_date_source", "current_due_date", "original_due_date", "due_date_rule_id"]: if _has_column("client_service_subscriptions", column_name): op.drop_column("client_service_subscriptions", column_name) if _has_table("service_due_date_extensions"): op.drop_table("service_due_date_extensions") if _has_table("service_due_date_rules"): op.drop_table("service_due_date_rules")