"""Phase 1 initial schema Revision ID: 001_initial Revises: None Create Date: 2026-06-30 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import JSONB, UUID revision: str = "001_initial" down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "erp_systems", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.String(255), nullable=False), sa.Column("erp_type", sa.String(50), nullable=False, index=True), sa.Column("version", sa.String(50)), sa.Column("base_url", sa.String(500)), sa.Column("auth_config", JSONB), sa.Column("status", sa.String(20), default="pending", index=True), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_table( "pharmacies", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("code", sa.String(50), unique=True, nullable=False, index=True), sa.Column("name", sa.String(255), nullable=False), sa.Column("address", sa.String(500)), sa.Column("city", sa.String(100)), sa.Column("province", sa.String(100)), sa.Column("postal_code", sa.String(20)), sa.Column("country", sa.String(2), default="ES"), sa.Column("latitude", sa.Float), sa.Column("longitude", sa.Float), sa.Column("phone", sa.String(50)), sa.Column("email", sa.String(255)), sa.Column("opening_hours", JSONB), sa.Column("status", sa.String(20), default="pending", index=True), sa.Column("erp_type", sa.String(50)), sa.Column("erp_version", sa.String(50)), sa.Column("sync_strategy", sa.String(20), default="incremental"), sa.Column("sync_interval_minutes", sa.Integer, default=30), sa.Column("last_sync_at", sa.DateTime(timezone=True)), sa.Column("last_sync_status", sa.String(20)), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_index("ix_pharmacies_location", "pharmacies", ["latitude", "longitude"]) op.create_index("ix_pharmacies_status_erp", "pharmacies", ["status", "erp_type"]) op.create_table( "connectors", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("erp_system_id", UUID(as_uuid=True), sa.ForeignKey("erp_systems.id"), nullable=False, index=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("name", sa.String(255), nullable=False), sa.Column("connector_type", sa.String(50), nullable=False, index=True), sa.Column("version", sa.String(20), default="1.0.0"), sa.Column("config", JSONB), sa.Column("status", sa.String(20), default="pending", index=True), sa.Column("last_heartbeat_at", sa.DateTime(timezone=True)), sa.Column("last_error", sa.Text), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), sa.UniqueConstraint("erp_system_id", "pharmacy_id", name="uq_connector_erp_pharmacy"), ) op.add_column("pharmacies", sa.Column("connector_id", UUID(as_uuid=True), sa.ForeignKey("connectors.id"), nullable=True)) op.create_index("ix_pharmacies_connector_id", "pharmacies", ["connector_id"]) op.create_table( "connector_versions", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("connector_id", UUID(as_uuid=True), sa.ForeignKey("connectors.id"), nullable=False, index=True), sa.Column("connector_type", sa.String(50), nullable=False, index=True), sa.Column("version", sa.String(20), nullable=False), sa.Column("changelog", sa.Text), sa.Column("download_url", sa.String(500)), sa.Column("checksum", sa.String(128)), sa.Column("is_latest", sa.Boolean, default=False, index=True), sa.Column("is_required", sa.Boolean, default=False), sa.Column("released_at", sa.DateTime(timezone=True)), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), sa.UniqueConstraint("connector_type", "version", name="uq_connector_version"), ) op.create_table( "connector_configs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("connector_id", UUID(as_uuid=True), sa.ForeignKey("connectors.id"), nullable=False, index=True), sa.Column("key", sa.String(100), nullable=False), sa.Column("value", sa.Text, nullable=False), sa.Column("encrypted", sa.Boolean, default=False), sa.Column("description", sa.String(500)), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), sa.UniqueConstraint("connector_id", "key", name="uq_connector_config_key"), ) op.create_table( "medications", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("nregistro", sa.String(50), unique=True, nullable=False, index=True), sa.Column("name", sa.String(500), nullable=False, index=True), sa.Column("active_ingredient", sa.String(500)), sa.Column("dosage", sa.String(100)), sa.Column("form", sa.String(100)), sa.Column("atc_code", sa.String(20), index=True), sa.Column("prescription_required", sa.Boolean, default=False), sa.Column("manufacturer", sa.String(255)), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_table( "medication_catalog", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("medication_id", UUID(as_uuid=True), sa.ForeignKey("medications.id"), nullable=False, index=True), sa.Column("price", sa.Float), sa.Column("stock", sa.Integer, default=0), sa.Column("is_available", sa.Boolean, default=False, index=True), sa.Column("last_verified_at", sa.DateTime(timezone=True)), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), sa.UniqueConstraint("pharmacy_id", "medication_id", name="uq_catalog_pharmacy_medication"), ) op.create_table( "inventory_records", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("medication_id", UUID(as_uuid=True), sa.ForeignKey("medications.id"), nullable=False, index=True), sa.Column("stock", sa.Integer, default=0), sa.Column("reserved_stock", sa.Integer, default=0), sa.Column("available_stock", sa.Integer, default=0), sa.Column("min_stock", sa.Integer), sa.Column("max_stock", sa.Integer), sa.Column("last_updated_at", sa.DateTime(timezone=True), index=True), sa.Column("source", sa.String(100)), sa.Column("batch_id", sa.String(100)), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), sa.UniqueConstraint("pharmacy_id", "medication_id", name="uq_inventory_pharmacy_medication"), ) op.create_table( "inventory_history", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("inventory_record_id", UUID(as_uuid=True), sa.ForeignKey("inventory_records.id"), nullable=False, index=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("medication_id", UUID(as_uuid=True), sa.ForeignKey("medications.id"), nullable=False, index=True), sa.Column("previous_stock", sa.Integer, nullable=False), sa.Column("new_stock", sa.Integer, nullable=False), sa.Column("delta", sa.Integer, nullable=False), sa.Column("reason", sa.String(255)), sa.Column("source", sa.String(100)), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True), ) op.create_table( "technical_users", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("username", sa.String(100), unique=True, nullable=False, index=True), sa.Column("email", sa.String(255)), sa.Column("full_name", sa.String(255)), sa.Column("password_hash", sa.String(255), nullable=False), sa.Column("role", sa.String(20), default="viewer", index=True), sa.Column("is_active", sa.Boolean, default=True, index=True), sa.Column("scopes", JSONB, default=list), sa.Column("last_login_at", sa.DateTime(timezone=True)), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_table( "api_clients", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.String(255), nullable=False), sa.Column("description", sa.Text), sa.Column("owner_id", UUID(as_uuid=True), sa.ForeignKey("technical_users.id"), nullable=True, index=True), sa.Column("is_active", sa.Boolean, default=True, index=True), sa.Column("scopes", JSONB, default=list), sa.Column("rate_limit", sa.Integer), sa.Column("allowed_ips", JSONB, default=list), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_table( "api_keys", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("api_client_id", UUID(as_uuid=True), sa.ForeignKey("api_clients.id"), nullable=False, index=True), sa.Column("key_prefix", sa.String(10), nullable=False, index=True), sa.Column("key_hash", sa.LargeBinary(64), nullable=False), sa.Column("name", sa.String(255), nullable=False), sa.Column("is_active", sa.Boolean, default=True, index=True), sa.Column("expires_at", sa.DateTime(timezone=True)), sa.Column("last_used_at", sa.DateTime(timezone=True)), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), ) op.create_index("ix_api_keys_prefix_active", "api_keys", ["key_prefix", "is_active"]) op.create_table( "reservations", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("medication_id", UUID(as_uuid=True), sa.ForeignKey("medications.id"), nullable=False, index=True), sa.Column("user_id", UUID(as_uuid=True), sa.ForeignKey("technical_users.id"), nullable=True, index=True), sa.Column("external_reference", sa.String(255)), sa.Column("quantity", sa.Integer, nullable=False), sa.Column("status", sa.String(20), default="pending", index=True), sa.Column("expires_at", sa.DateTime(timezone=True), index=True), sa.Column("confirmed_at", sa.DateTime(timezone=True)), sa.Column("cancelled_at", sa.DateTime(timezone=True)), sa.Column("idempotency_key", sa.String(255), unique=True, index=True), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_index("ix_reservations_pharmacy_status", "reservations", ["pharmacy_id", "status"]) op.create_index("ix_reservations_expires", "reservations", ["expires_at"]) op.create_table( "sync_jobs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("pharmacy_id", UUID(as_uuid=True), sa.ForeignKey("pharmacies.id"), nullable=False, index=True), sa.Column("connector_id", UUID(as_uuid=True), sa.ForeignKey("connectors.id"), nullable=False, index=True), sa.Column("strategy", sa.String(20), nullable=False), sa.Column("status", sa.String(20), default="pending", index=True), sa.Column("started_at", sa.DateTime(timezone=True)), sa.Column("completed_at", sa.DateTime(timezone=True)), sa.Column("items_total", sa.Integer), sa.Column("items_processed", sa.Integer), sa.Column("items_failed", sa.Integer), sa.Column("error_message", sa.Text), sa.Column("metadata", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), ) op.create_table( "sync_logs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("sync_job_id", UUID(as_uuid=True), sa.ForeignKey("sync_jobs.id"), nullable=False, index=True), sa.Column("level", sa.String(20), default="INFO", nullable=False), sa.Column("message", sa.Text, nullable=False), sa.Column("details", JSONB), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True), ) op.create_table( "audit_logs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("actor_id", UUID(as_uuid=True), index=True), sa.Column("actor_type", sa.String(50)), sa.Column("action", sa.String(100), nullable=False, index=True), sa.Column("resource_type", sa.String(100), nullable=False, index=True), sa.Column("resource_id", sa.String(255), index=True), sa.Column("old_value", JSONB), sa.Column("new_value", JSONB), sa.Column("ip_address", sa.String(45)), sa.Column("user_agent", sa.String(500)), sa.Column("request_id", sa.String(100), index=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, index=True), ) op.create_index("ix_audit_logs_action_resource", "audit_logs", ["action", "resource_type"]) op.create_index("ix_audit_logs_actor_created", "audit_logs", ["actor_id", "created_at"]) def downgrade() -> None: op.drop_table("audit_logs") op.drop_table("sync_logs") op.drop_table("sync_jobs") op.drop_table("reservations") op.drop_table("api_keys") op.drop_table("api_clients") op.drop_table("technical_users") op.drop_table("inventory_history") op.drop_table("inventory_records") op.drop_table("medication_catalog") op.drop_table("medications") op.drop_table("connector_configs") op.drop_table("connector_versions") op.drop_column("pharmacies", "connector_id") op.drop_table("connectors") op.drop_table("pharmacies") op.drop_table("erp_systems")