From 819d039b71b3464773c3596f01132d5b4d61ebfb Mon Sep 17 00:00:00 2001 From: Jens <1742418+1cu@users.noreply.github.com> Date: Sun, 5 Jul 2026 00:51:25 +0200 Subject: [PATCH] fix: persist created_on on first publish (#1204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## â„šī¸ Description - Link to the related issue(s): Issue # - Fixes first-publish persistence so newly published ads write `created_on` when missing. ## 📋 Changes Summary - Capture first-publish state before writing the new ad ID. - Persist one publication timestamp to both `updated_on` and missing first-publish `created_on`. - Add regression tests for first publish with an already-populated ad ID and update preservation of existing `created_on`. - No dependency, configuration, or generated artifact changes. ### âš™ī¸ Type of Change - [x] 🐞 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (adds new functionality without breaking existing usage) - [ ] đŸ’Ĩ Breaking change (changes that might break existing user setups, scripts, or configurations) ## ✅ Checklist - [x] I have reviewed my changes to ensure they meet the project's standards. - [x] I have tested my changes and ensured that all tests pass (`pdm run test`). - [x] I have formatted the code (`pdm run format`). - [x] I have verified that linting passes (`pdm run lint`). - [x] I have updated documentation where necessary. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. ## Summary by CodeRabbit * **Bug Fixes** * Improved publish timestamp handling so first-time publishes and later updates record dates consistently. * Preserves the original creation date when it already exists, while keeping the last-updated time current. * **Tests** * Added coverage for timestamp behavior on first publish and subsequent updates. * Verified date values are stored in ISO-8601 format with timezone information. --- .../publishing_persistence.py | 8 +- tests/unit/test_publishing_persistence.py | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/kleinanzeigen_bot/publishing_persistence.py b/src/kleinanzeigen_bot/publishing_persistence.py index 5d1e6d0..77731b6 100644 --- a/src/kleinanzeigen_bot/publishing_persistence.py +++ b/src/kleinanzeigen_bot/publishing_persistence.py @@ -72,6 +72,7 @@ def persist_published_ad( ) -> None: """Write the published ad ID, hash, timestamps, and counters back to the YAML file, then rename local paths to match the new ID.""" + is_first_publish = old_ad_id is None ad_cfg_orig["id"] = ad_id # Rename referenced images before hashing/saving so the YAML content and # content_hash reflect only image file renames that actually succeeded. @@ -89,9 +90,10 @@ def persist_published_ad( # Update content hash after successful publication # Calculate hash on original config to ensure consistent comparison on restart ad_cfg_orig["content_hash"] = AdPartial.model_validate(ad_cfg_orig).update_content_hash().content_hash - ad_cfg_orig["updated_on"] = _misc.now().isoformat(timespec = "seconds") - if not ad_cfg.created_on and not ad_cfg.id: - ad_cfg_orig["created_on"] = ad_cfg_orig["updated_on"] + published_at = _misc.now().isoformat(timespec = "seconds") + ad_cfg_orig["updated_on"] = published_at + if is_first_publish and not ad_cfg.created_on: + ad_cfg_orig["created_on"] = published_at # Increment repost_count only for REPLACE operations (actual reposts) if mode == AdUpdateStrategy.REPLACE: diff --git a/tests/unit/test_publishing_persistence.py b/tests/unit/test_publishing_persistence.py index bb28271..8797ab5 100644 --- a/tests/unit/test_publishing_persistence.py +++ b/tests/unit/test_publishing_persistence.py @@ -3,6 +3,7 @@ # SPDX-ArtifactOfProjectHomePage: https://github.com/Second-Hand-Friends/kleinanzeigen-bot/ """Tests for publishing persistence functionality.""" +from datetime import datetime, timezone from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -144,6 +145,90 @@ class TestLogLocalPathRenameResult: assert mock_warning.called == expect_warning +class TestPersistPublishedAdTimestamps: + """Tests for timestamp persistence in persist_published_ad.""" + + @staticmethod + def _make_ad_cfg_orig(*, created_on:str | None = None) -> dict[str, Any]: + """Minimal ad_cfg_orig with required fields for AdPartial validation.""" + ad_cfg_orig:dict[str, Any] = { + "title": "Test Ad Title", + "description": "Test description for the ad listing.", + "type": "OFFER", + "category": "160", + } + if created_on is not None: + ad_cfg_orig["created_on"] = created_on + return ad_cfg_orig + + def test_first_publish_sets_created_on_when_ad_already_has_id(self) -> None: + """First publish sets created_on based on old_ad_id, not current ad_cfg.id.""" + ad = _make_min_ad() + ad.id = 12345 + ad_cfg_orig = self._make_ad_cfg_orig() + cfg = _make_config() + published_at = datetime(2026, 7, 4, 12, 34, 56, tzinfo = timezone.utc) + + with ( + patch("kleinanzeigen_bot.local_path_renaming.rename_referenced_local_image_files_after_id_change", + return_value = _make_image_rename_result()), + patch("kleinanzeigen_bot.local_path_renaming.rename_local_ad_file_and_folder_after_id_change", + return_value = _local_path_renaming.LocalPathRenameResult( + ad_file = Path("test.yaml"), + file_status = RenameStatus.SAME, + folder_status = RenameStatus.SAME, + )), + patch("kleinanzeigen_bot.utils.dicts.save_dict"), + patch("kleinanzeigen_bot.utils.misc.now", return_value = published_at), + ): + publishing_persistence.persist_published_ad( + ad_file = "test.yaml", + ad_cfg = ad, + ad_cfg_orig = ad_cfg_orig, + old_ad_id = None, + ad_id = 12345, + mode = AdUpdateStrategy.REPLACE, + config = cfg, + ) + + assert ad_cfg_orig["created_on"] == "2026-07-04T12:34:56+00:00" + assert ad_cfg_orig["updated_on"] == "2026-07-04T12:34:56+00:00" + + def test_update_preserves_existing_created_on(self) -> None: + """Updating an existing ad keeps the original created_on value.""" + ad = _make_min_ad() + ad.id = 12345 + ad.created_on = datetime(2024, 1, 2, 3, 4, 5, tzinfo = timezone.utc) + ad_cfg_orig = self._make_ad_cfg_orig(created_on = "2024-01-02T03:04:05") + cfg = _make_config() + updated_at = datetime(2026, 7, 4, 12, 34, 56, tzinfo = timezone.utc) + + with ( + patch("kleinanzeigen_bot.local_path_renaming.rename_referenced_local_image_files_after_id_change", + return_value = _make_image_rename_result()), + patch("kleinanzeigen_bot.local_path_renaming.rename_local_ad_file_and_folder_after_id_change", + return_value = _local_path_renaming.LocalPathRenameResult( + ad_file = Path("test.yaml"), + file_status = RenameStatus.SAME, + folder_status = RenameStatus.SAME, + )), + patch("kleinanzeigen_bot.utils.dicts.save_dict"), + patch("kleinanzeigen_bot.utils.misc.now", return_value = updated_at), + ): + publishing_persistence.persist_published_ad( + ad_file = "test.yaml", + ad_cfg = ad, + ad_cfg_orig = ad_cfg_orig, + old_ad_id = 12345, + ad_id = 12345, + mode = AdUpdateStrategy.MODIFY, + config = cfg, + ) + + assert ad_cfg_orig["created_on"] == "2024-01-02T03:04:05" + assert ad_cfg_orig["updated_on"] == "2026-07-04T12:34:56+00:00" + + class TestPersistPublishedAdPriceReductionCount: """Tests for price_reduction_count persistence in persist_published_ad."""