diff --git a/backend/app/models/time_entry.py b/backend/app/models/time_entry.py index 0cee538..b08692b 100644 --- a/backend/app/models/time_entry.py +++ b/backend/app/models/time_entry.py @@ -36,7 +36,7 @@ class TimeEntry(Base): date: Mapped[date] = mapped_column(Date, nullable=False, index=True) start_time: Mapped[time] = mapped_column(Time(timezone=False), nullable=False) end_time: Mapped[time | None] = mapped_column(Time(timezone=False)) - break_minutes: Mapped[int] = mapped_column(Integer, default=0) + break_minutes: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0") break_start: Mapped[time | None] = mapped_column(Time(timezone=False)) # Aktive Pause tracken project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True)) note: Mapped[str | None] = mapped_column(Text) diff --git a/backend/migrations/versions/0044_time_entries_break_minutes_default.py b/backend/migrations/versions/0044_time_entries_break_minutes_default.py new file mode 100644 index 0000000..d8fdba8 --- /dev/null +++ b/backend/migrations/versions/0044_time_entries_break_minutes_default.py @@ -0,0 +1,25 @@ +"""time_entries.break_minutes: DB-Default ergänzen (Model-Sync) + +Revision ID: 0044 +Revises: 0043 +Create Date: 2026-08-27 + +Model hatte nur Python-seitigen default=0, kein server_default (wie zuvor +hours_payouts.status, siehe Commit 71e3e82). Column ist bereits NOT NULL, +hier nur DEFAULT nachgezogen für Konsistenz mit dem Model. +""" +from alembic import op +from sqlalchemy import text + +revision = "0044" +down_revision = "0043" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute(text("ALTER TABLE time_entries ALTER COLUMN break_minutes SET DEFAULT 0")) + + +def downgrade() -> None: + op.execute(text("ALTER TABLE time_entries ALTER COLUMN break_minutes DROP DEFAULT")) diff --git a/backend/tests/test_retention.py b/backend/tests/test_retention.py index e9b9a62..6d85451 100644 --- a/backend/tests/test_retention.py +++ b/backend/tests/test_retention.py @@ -21,7 +21,9 @@ async def retention_headers(client: AsyncClient): }) assert resp.status_code == 201, resp.text data = resp.json() - return {"Authorization": f"Bearer {data['access_token']}"}, data["user"]["company_id"] + headers = {"Authorization": f"Bearer {data['access_token']}"} + me = await client.get("/api/v1/users/me", headers=headers) + return headers, me.json()["company_id"] @pytest.mark.asyncio(loop_scope="session") @@ -35,20 +37,23 @@ async def test_purge_deletes_old_time_entries_respects_retention( old_date = date.today() - timedelta(days=11 * 365) # älter als Default 10 Jahre recent_date = date.today() - timedelta(days=30) + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) await db_session.execute(text( - "INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) " - "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')" + "INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) " + "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')" ), {"uid": user_id, "d": old_date}) await db_session.execute(text( - "INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) " - "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')" + "INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) " + "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')" ), {"uid": user_id, "d": recent_date}) await db_session.commit() + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) result = await run_retention_purge(db_session, company_id=company_id) await db_session.commit() assert result["time_entries"] == 1 + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) remaining = await db_session.execute(text( "SELECT date FROM time_entries WHERE user_id = :uid" ), {"uid": user_id}) @@ -70,12 +75,14 @@ async def test_purge_configurable_retention_years( user_id = me.json()["id"] three_years_ago = date.today() - timedelta(days=3 * 365) + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) await db_session.execute(text( - "INSERT INTO time_entries (id, user_id, date, start_time, end_time, status, source) " - "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 'approved', 'web')" + "INSERT INTO time_entries (id, user_id, date, start_time, end_time, break_minutes, status, source) " + "VALUES (gen_random_uuid(), :uid, :d, '08:00', '16:00', 0, 'approved', 'web')" ), {"uid": user_id, "d": three_years_ago}) await db_session.commit() + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) result = await run_retention_purge(db_session, company_id=company_id) await db_session.commit() assert result["time_entries"] >= 1 @@ -92,6 +99,7 @@ async def test_purge_expired_sessions_and_audit_logs_global( expired = datetime.now(timezone.utc) - timedelta(days=1) old_audit = datetime.now(timezone.utc) - timedelta(days=4 * 365) + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) await db_session.execute(text( "INSERT INTO sessions (id, user_id, refresh_token_hash, expires_at) " "VALUES (gen_random_uuid(), :uid, :h, :exp)" @@ -102,6 +110,7 @@ async def test_purge_expired_sessions_and_audit_logs_global( ), {"cid": company_id, "uid": user_id, "ts": old_audit}) await db_session.commit() + await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) result = await run_retention_purge(db_session, company_id=None) await db_session.commit() assert result["sessions"] >= 1