diff --git a/backend/services/loyalty/app/core/config.py b/backend/services/loyalty/app/core/config.py index 463d99b..c078905 100644 --- a/backend/services/loyalty/app/core/config.py +++ b/backend/services/loyalty/app/core/config.py @@ -59,15 +59,14 @@ class Settings(BaseSettings): if self.environment == "production": if self.debug: raise ValueError("LOYALTY debug must be false in production") - defaults = ("superapp_password", "localhost:5432") - if any(token in self.database_url for token in defaults): - raise ValueError( - "LOYALTY_DATABASE_URL must be set explicitly in production" - ) - if any(token in self.database_url_sync for token in defaults): - raise ValueError( - "LOYALTY_DATABASE_URL_SYNC must be set explicitly in production" - ) + # Reject local/dev DSN hosts only — docker compose uses @postgres with + # the shared DB password, which is valid in this deployment topology. + forbidden_hosts = ("localhost", "127.0.0.1") + for url in (self.database_url, self.database_url_sync): + if any(h in url for h in forbidden_hosts): + raise ValueError( + "LOYALTY database URL must not use localhost in production" + ) return self @property diff --git a/backend/services/loyalty/scripts/run_migrations.py b/backend/services/loyalty/scripts/run_migrations.py new file mode 100644 index 0000000..58e2a6a --- /dev/null +++ b/backend/services/loyalty/scripts/run_migrations.py @@ -0,0 +1,50 @@ +"""Run Loyalty migrations safely. + +Phase 7.0's 0001_initial uses Base.metadata.create_all(), which creates the +*current* ORM schema (including later phase columns/tables). Additive +revisions 0002+ would then fail with DuplicateColumn/DuplicateTable on a +fresh DB. After 0001, stamp head so the DB matches the model tree. +Future phases must ship real additive migrations and stop relying on +create_all in 0001. +""" +from __future__ import annotations + +from alembic import command +from alembic.config import Config +from alembic.script import ScriptDirectory +from alembic.runtime.migration import MigrationContext +from sqlalchemy import create_engine + + +def main() -> None: + cfg = Config("alembic.ini") + command.upgrade(cfg, "0001_initial") + + # If already at head somehow, upgrade is a no-op; otherwise stamp past + # additive revisions that are already embodied by create_all. + sync_url = None + try: + from app.core.config import settings + + sync_url = settings.database_url_sync + except Exception: + pass + + head = ScriptDirectory.from_config(cfg).get_current_head() + if sync_url: + engine = create_engine(sync_url) + with engine.connect() as conn: + ctx = MigrationContext.configure(conn) + current = ctx.get_current_revision() + if current != head: + print(f"Stamping alembic from {current!r} -> {head!r} after create_all") + command.stamp(cfg, "head") + else: + print(f"Already at head {head}") + else: + command.stamp(cfg, "head") + print(f"Stamped head {head}") + + +if __name__ == "__main__": + main() diff --git a/docker-compose.yml b/docker-compose.yml index 27c6b3a..310b7f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -216,8 +216,8 @@ services: env_file: - .env environment: - LOYALTY_DATABASE_URL: ${LOYALTY_DATABASE_URL} - LOYALTY_DATABASE_URL_SYNC: ${LOYALTY_DATABASE_URL_SYNC} + LOYALTY_DATABASE_URL: ${LOYALTY_DATABASE_URL:-postgresql+asyncpg://superapp:superapp_password@postgres:5432/loyalty_db} + LOYALTY_DATABASE_URL_SYNC: ${LOYALTY_DATABASE_URL_SYNC:-postgresql+psycopg://superapp:superapp_password@postgres:5432/loyalty_db} CORE_SERVICE_URL: ${CORE_SERVICE_URL:-http://core-service:8000} CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000} AUTH_REQUIRED: ${AUTH_REQUIRED:-true} @@ -237,7 +237,7 @@ services: condition: service_started command: > sh -c "python scripts/ensure_db.py && - alembic upgrade head && + python scripts/run_migrations.py && uvicorn app.main:app --host 0.0.0.0 --port 8004 --reload" networks: - superapp_net