0001_initial.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Initial schema — create all PMDI tables.
  2. Revision ID: 0001_initial
  3. Revises:
  4. Create Date: 2026-08-31 00:00:00.000000
  5. Creates:
  6. · admin_users
  7. · clients
  8. · email_templates
  9. · invoices (with PostgreSQL ARRAY(Integer) for notification_dates)
  10. · notification_logs (with JSONB context column)
  11. """
  12. from __future__ import annotations
  13. from typing import Sequence, Union
  14. import sqlalchemy as sa
  15. from alembic import op
  16. from sqlalchemy.dialects import postgresql
  17. # revision identifiers
  18. revision: str = "0001_initial"
  19. down_revision: Union[str, None] = None
  20. branch_labels: Union[str, Sequence[str], None] = None
  21. depends_on: Union[str, Sequence[str], None] = None
  22. def upgrade() -> None:
  23. # ── admin_users ────────────────────────────────────────────────────────
  24. op.create_table(
  25. "admin_users",
  26. sa.Column("id", sa.Integer(), nullable=False),
  27. sa.Column("username", sa.String(64), nullable=False),
  28. sa.Column("hashed_password", sa.String(256), nullable=False),
  29. sa.Column("totp_secret", sa.String(64), nullable=True),
  30. sa.Column("mfa_enabled", sa.Boolean(), nullable=False, server_default="false"),
  31. sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
  32. sa.PrimaryKeyConstraint("id"),
  33. sa.UniqueConstraint("username"),
  34. )
  35. op.create_index("ix_admin_users_id", "admin_users", ["id"], unique=False)
  36. op.create_index("ix_admin_users_username", "admin_users", ["username"], unique=True)
  37. # ── clients ────────────────────────────────────────────────────────────
  38. op.create_table(
  39. "clients",
  40. sa.Column("id", sa.Integer(), nullable=False),
  41. sa.Column("client_id_name", sa.String(64), nullable=False),
  42. sa.Column("client_name", sa.String(256), nullable=False),
  43. sa.Column("client_email", sa.String(256), nullable=False),
  44. sa.Column("created_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
  45. sa.PrimaryKeyConstraint("id"),
  46. sa.UniqueConstraint("client_id_name"),
  47. )
  48. op.create_index("ix_clients_id", "clients", ["id"], unique=False)
  49. # ── email_templates ────────────────────────────────────────────────────
  50. op.create_table(
  51. "email_templates",
  52. sa.Column("id", sa.Integer(), nullable=False),
  53. sa.Column("name", sa.String(128), nullable=False),
  54. sa.Column("template_type", sa.String(32), nullable=False),
  55. sa.Column("subject", sa.String(256), nullable=False),
  56. sa.Column("template_body", sa.Text(), nullable=False),
  57. sa.Column("created_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
  58. sa.PrimaryKeyConstraint("id"),
  59. )
  60. op.create_index("ix_email_templates_id", "email_templates", ["id"], unique=False)
  61. # ── invoices ───────────────────────────────────────────────────────────
  62. op.create_table(
  63. "invoices",
  64. sa.Column("id", sa.Integer(), nullable=False),
  65. sa.Column("invoice_number", sa.String(64), nullable=False),
  66. sa.Column("client_id", sa.Integer(), nullable=False),
  67. sa.Column("pay_date", sa.Date(), nullable=False),
  68. sa.Column("template_id", sa.Integer(), nullable=False),
  69. sa.Column("notification_dates", postgresql.ARRAY(sa.Integer()), nullable=True),
  70. sa.Column("overdue_recurring_days", sa.Integer(), nullable=False, server_default="0"),
  71. sa.Column("paid", sa.Boolean(), nullable=False, server_default="false"),
  72. sa.Column("created_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
  73. sa.ForeignKeyConstraint(["client_id"], ["clients.id"], ondelete="RESTRICT"),
  74. sa.ForeignKeyConstraint(["template_id"], ["email_templates.id"], ondelete="RESTRICT"),
  75. sa.PrimaryKeyConstraint("id"),
  76. sa.UniqueConstraint("invoice_number"),
  77. )
  78. op.create_index("ix_invoices_id", "invoices", ["id"], unique=False)
  79. # ── notification_logs ──────────────────────────────────────────────────
  80. op.create_table(
  81. "notification_logs",
  82. sa.Column("id", sa.Integer(), nullable=False),
  83. sa.Column("invoice_id", sa.Integer(), nullable=False),
  84. sa.Column("status", sa.String(64), nullable=False),
  85. sa.Column("sent_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
  86. sa.Column("context", postgresql.JSONB(), nullable=True),
  87. sa.Column("error_message", sa.Text(), nullable=True),
  88. sa.ForeignKeyConstraint(["invoice_id"], ["invoices.id"], ondelete="CASCADE"),
  89. sa.PrimaryKeyConstraint("id"),
  90. )
  91. op.create_index("ix_notification_logs_id", "notification_logs", ["id"], unique=False)
  92. def downgrade() -> None:
  93. op.drop_index("ix_notification_logs_id", table_name="notification_logs")
  94. op.drop_table("notification_logs")
  95. op.drop_index("ix_invoices_id", table_name="invoices")
  96. op.drop_table("invoices")
  97. op.drop_index("ix_email_templates_id", table_name="email_templates")
  98. op.drop_table("email_templates")
  99. op.drop_index("ix_clients_id", table_name="clients")
  100. op.drop_table("clients")
  101. op.drop_index("ix_admin_users_username", table_name="admin_users")
  102. op.drop_index("ix_admin_users_id", table_name="admin_users")
  103. op.drop_table("admin_users")