Add phase 6 PAN login invitation and client identity

This commit is contained in:
A R R R Associates
2026-07-23 15:08:26 +05:30
parent 90345a2e1a
commit 76039d915d
13 changed files with 332 additions and 10 deletions
@@ -0,0 +1,44 @@
"""Phase 6 PAN login, invitation and client identity.
Revision ID: 20260723_phase6_pan_login_client_identity
Revises: 20260723_phase5_secure_credential_vault
"""
from alembic import op
import sqlalchemy as sa
revision="20260723_phase6_pan_login_client_identity"
down_revision="20260723_phase5_secure_credential_vault"
branch_labels=None
depends_on=None
def upgrade():
op.create_table("client_portal_identities",
sa.Column("id",sa.Integer(),primary_key=True),
sa.Column("tenant_id",sa.Integer(),sa.ForeignKey("tenants.id",ondelete="CASCADE"),nullable=False),
sa.Column("branch_id",sa.Integer(),sa.ForeignKey("branches.id",ondelete="CASCADE"),nullable=False),
sa.Column("client_id",sa.Integer(),sa.ForeignKey("clients.id",ondelete="CASCADE"),nullable=False),
sa.Column("user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="SET NULL")),
sa.Column("normalized_pan",sa.String(10),nullable=False),
sa.Column("login_email",sa.String(255),nullable=False),
sa.Column("email_is_placeholder",sa.Boolean(),nullable=False,server_default=sa.false()),
sa.Column("status",sa.String(30),nullable=False,server_default="not_invited"),
sa.Column("invited_at_utc",sa.DateTime(timezone=True)),
sa.Column("activated_at_utc",sa.DateTime(timezone=True)),
sa.Column("last_invited_at_utc",sa.DateTime(timezone=True)),
sa.Column("created_by_user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="SET NULL")),
sa.Column("updated_by_user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="SET NULL")),
sa.Column("created_at_utc",sa.DateTime(timezone=True),nullable=False),
sa.Column("updated_at_utc",sa.DateTime(timezone=True),nullable=False),
sa.UniqueConstraint("client_id",name="uq_client_portal_identity_client"),
sa.UniqueConstraint("tenant_id","normalized_pan",name="uq_client_portal_identity_tenant_pan"))
for c in ["tenant_id","branch_id","client_id","user_id","normalized_pan","login_email","status"]: op.create_index(f"ix_client_portal_identities_{c}","client_portal_identities",[c])
op.create_table("client_portal_invitations",
sa.Column("id",sa.Integer(),primary_key=True),sa.Column("identity_id",sa.Integer(),sa.ForeignKey("client_portal_identities.id",ondelete="CASCADE"),nullable=False),sa.Column("user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="CASCADE"),nullable=False),sa.Column("delivery_mode",sa.String(30),nullable=False,server_default="email"),sa.Column("recipient_email",sa.String(255)),sa.Column("status",sa.String(30),nullable=False,server_default="issued"),sa.Column("issued_by_user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="SET NULL")),sa.Column("issued_at_utc",sa.DateTime(timezone=True),nullable=False),sa.Column("expires_at_utc",sa.DateTime(timezone=True)),sa.Column("accepted_at_utc",sa.DateTime(timezone=True)),sa.Column("note",sa.Text()))
for c in ["identity_id","user_id","status"]: op.create_index(f"ix_client_portal_invitations_{c}","client_portal_invitations",[c])
op.create_table("client_identity_audit_logs",
sa.Column("id",sa.Integer(),primary_key=True),sa.Column("tenant_id",sa.Integer(),sa.ForeignKey("tenants.id",ondelete="CASCADE"),nullable=False),sa.Column("client_id",sa.Integer(),sa.ForeignKey("clients.id",ondelete="CASCADE"),nullable=False),sa.Column("identity_id",sa.Integer(),sa.ForeignKey("client_portal_identities.id",ondelete="SET NULL")),sa.Column("actor_user_id",sa.Integer(),sa.ForeignKey("users.id",ondelete="SET NULL")),sa.Column("action",sa.String(50),nullable=False),sa.Column("summary",sa.String(255),nullable=False),sa.Column("created_at_utc",sa.DateTime(timezone=True),nullable=False))
for c in ["tenant_id","client_id","identity_id","action"]: op.create_index(f"ix_client_identity_audit_logs_{c}","client_identity_audit_logs",[c])
def downgrade():
op.drop_table("client_identity_audit_logs")
op.drop_table("client_portal_invitations")
op.drop_table("client_portal_identities")