diff --git a/backend/alembic/versions/005_add_disclosure_requests.py b/backend/alembic/versions/005_add_disclosure_requests.py new file mode 100644 index 0000000..a6a4aea --- /dev/null +++ b/backend/alembic/versions/005_add_disclosure_requests.py @@ -0,0 +1,72 @@ +"""add disclosure_requests table + +Revision ID: 005_disclosure +Revises: 5717043d0f9d +Create Date: 2026-02-26 14:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "005_disclosure" +down_revision: Union[str, None] = "5717043d0f9d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute( + """ + CREATE TABLE disclosure_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + case_id INT NOT NULL, + requester_id INT NOT NULL, + reason VARCHAR(500) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'pending', + reviewed_by INT NULL, + reviewed_at DATETIME NULL, + expires_at DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_dr_case FOREIGN KEY (case_id) REFERENCES cases(id), + CONSTRAINT fk_dr_requester FOREIGN KEY (requester_id) REFERENCES users(id), + CONSTRAINT fk_dr_reviewer FOREIGN KEY (reviewed_by) REFERENCES users(id), + CONSTRAINT chk_dr_status CHECK (status IN ('pending', 'approved', 'rejected')), + INDEX idx_dr_case_status (case_id, status), + INDEX idx_dr_requester (requester_id), + INDEX idx_dr_status (status) + ) + """ + ) + + # Update Notification CHECK constraint to include disclosure types + op.execute("ALTER TABLE notifications DROP CONSTRAINT chk_notif") + op.execute( + """ + ALTER TABLE notifications ADD CONSTRAINT chk_notif CHECK ( + notification_type IN ( + 'new_cases_uploaded','icd_entered','icd_uploaded', + 'report_ready','coding_completed','disclosure_request','disclosure_resolved' + ) + ) + """ + ) + + +def downgrade() -> None: + # Restore original Notification CHECK constraint + op.execute("ALTER TABLE notifications DROP CONSTRAINT chk_notif") + op.execute( + """ + ALTER TABLE notifications ADD CONSTRAINT chk_notif CHECK ( + notification_type IN ( + 'new_cases_uploaded','icd_entered','icd_uploaded', + 'report_ready','coding_completed' + ) + ) + """ + ) + + op.execute("DROP TABLE disclosure_requests") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 2bdb7c5..dd784a1 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -3,7 +3,7 @@ from app.models.user import AllowedDomain, InvitationLink, RefreshToken, User from app.models.case import Case, CaseICDCode from app.models.report import WeeklyReport, YearlySummary -from app.models.audit import AuditLog, ImportLog, Notification +from app.models.audit import AuditLog, DisclosureRequest, ImportLog, Notification __all__ = [ # User & Auth @@ -21,4 +21,6 @@ __all__ = [ "ImportLog", "AuditLog", "Notification", + # Disclosure / Data Access + "DisclosureRequest", ] diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py index 9e8b460..3963567 100644 --- a/backend/app/models/audit.py +++ b/backend/app/models/audit.py @@ -124,7 +124,51 @@ class Notification(Base): CheckConstraint( "notification_type IN (" "'new_cases_uploaded','icd_entered','icd_uploaded'," - "'report_ready','coding_completed')", + "'report_ready','coding_completed'," + "'disclosure_request','disclosure_resolved')", name="chk_notif", ), ) + + +class DisclosureRequest(Base): + __tablename__ = "disclosure_requests" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + case_id: Mapped[int] = mapped_column( + Integer, ForeignKey("cases.id"), nullable=False + ) + requester_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id"), nullable=False + ) + reason: Mapped[str] = mapped_column(String(500), nullable=False) + status: Mapped[str] = mapped_column( + String(20), nullable=False, server_default="pending" + ) + reviewed_by: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("users.id"), nullable=True + ) + reviewed_at: Mapped[Optional[dt.datetime]] = mapped_column( + DateTime, nullable=True + ) + expires_at: Mapped[Optional[dt.datetime]] = mapped_column( + DateTime, nullable=True + ) + created_at: Mapped[dt.datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + + # Relationships + case: Mapped["Case"] = relationship(foreign_keys=[case_id]) + requester: Mapped["User"] = relationship(foreign_keys=[requester_id]) + reviewer: Mapped[Optional["User"]] = relationship(foreign_keys=[reviewed_by]) + + __table_args__ = ( + CheckConstraint( + "status IN ('pending','approved','rejected')", + name="chk_dr_status", + ), + Index("idx_dr_case_status", "case_id", "status"), + Index("idx_dr_requester", "requester_id"), + Index("idx_dr_status", "status"), + )