fix: persist created_on on first publish (#1204)

## ℹ️ 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.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jens
2026-07-05 00:51:25 +02:00
committed by GitHub
parent 9cffbf6e8d
commit 819d039b71
2 changed files with 90 additions and 3 deletions

View File

@@ -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:

View File

@@ -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."""