Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ download:
preserve_local_settings: true # if true, preserve local-only settings when re-downloading an already saved ad (default: true)
```

- If an ad is eligible for direct purchase but no predefined shipping option can be extracted (for example, no price match or all matches are excluded), the download saves `sell_directly: false` and `shipping_options: []` with a warning. Extracted shipping costs are retained. Review the shipping options before enabling direct purchase again; no carrier is guessed for this fallback, and `ad_defaults.sell_directly` does not re-enable it.
- `download.dir` controls where `download` writes ad files.
- Leaving `download.dir` at the default literal `downloaded-ads` keeps workspace-mode behavior (portable workspace folder in portable mode, XDG workspace folder in XDG mode).
- Custom relative `download.dir` values are resolved relative to `config.yaml`, not the current shell working directory.
Expand Down
9 changes: 9 additions & 0 deletions src/kleinanzeigen_bot/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,15 @@ async def _extract_ad_page_info(
info["price"], info["price_type"] = await self._extract_pricing_info_from_ad_page(island_props = island_props)
info["shipping_type"], info["shipping_costs"], info["shipping_options"] = await self._extract_shipping_info_from_ad_page(island_props = island_props)
info["sell_directly"] = await self._extract_sell_directly_from_ad_page()
if info["sell_directly"] and not info["shipping_options"]:
# Eligibility does not identify a carrier. Keep the extracted costs without
# inventing a predefined option or inheriting unrelated ad defaults.
info["sell_directly"] = False
info["shipping_options"] = []
LOG.warning(
"Ad %s: no predefined shipping option could be extracted. Saved with sell_directly=false; "
"review shipping_options before enabling direct purchase.", ad_id,
)
info["images"] = await self._download_images_from_ad_page(directory, ad_file_stem, island_props = island_props)
info["contact"] = await self._extract_contact_from_ad_page(island_props = island_props)
info["id"] = ad_id
Expand Down
2 changes: 2 additions & 0 deletions src/kleinanzeigen_bot/resources/translations.de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ kleinanzeigen_bot/extract.py:
"Could not remove staging directory %s: %s": "Konnte Staging-Verzeichnis %s nicht entfernen: %s"

_extract_ad_page_info:
? "Ad %s: no predefined shipping option could be extracted. Saved with sell_directly=false; review shipping_options before enabling direct purchase."
: "Anzeige %s: Keine vordefinierte Versandoption konnte ermittelt werden. Mit sell_directly=false gespeichert; shipping_options vor dem Aktivieren von Direkt kaufen prüfen."
"Could not extract creation date from any selector or Astro component data.": "Erstellungsdatum konnte weder über Selektoren noch über Astro-Komponentendaten extrahiert werden."
? "Ad %s: neither BelenConf nor the anonymous fallback delivered ad dimensions; category attributes may stay incomplete and publishing can fail."
: "Für Anzeige %s haben weder BelenConf noch der anonyme Fallback Anzeigen-Dimensionen geliefert; Kategorie-Attribute können unvollständig bleiben und das Veröffentlichen kann fehlschlagen."
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,41 @@ async def test_extract_description_with_affixes_no_affixes(self, test_extractor:
ad_cfg, _staging_dir, _final_dir, _ad_file_stem = await test_extractor._extract_ad_page_info_with_directory_handling(base_dir, 12345)
assert ad_cfg.description == raw_description

@pytest.mark.asyncio
@pytest.mark.parametrize(("options", "eligible", "expected", "expected_options", "warned"), [
(None, True, False, [], True),
([], True, False, [], True),
(["DHL_2"], True, True, ["DHL_2"], False),
(None, False, False, None, False),
])
async def test_extract_direct_purchase_with_unresolved_shipping(
self, test_extractor:extract_module.AdExtractor, tmp_path:Path, caplog:pytest.LogCaptureFixture, *,
options:list[str] | None, eligible:bool, expected:bool, expected_options:list[str] | None, warned:bool,
) -> None:
"""Unresolved shipping must not discard the ad, invent a carrier, or let ad defaults re-enable direct purchase."""
test_extractor.config = test_extractor.config.with_values({"ad_defaults": {"sell_directly": True}})
test_extractor.page = MagicMock(url = "https://www.kleinanzeigen.de/s-anzeige/test/12345")
with patch.multiple(
test_extractor,
web_text = AsyncMock(side_effect = ["Test Title", "Original Description", "03.02.2025"]),
web_probe = AsyncMock(return_value = None),
web_execute = AsyncMock(return_value = {"universalAnalyticsOpts": {"dimensions": {"l3_category_id": "", "ad_attributes": ""}}}),
_extract_category_from_ad_page = AsyncMock(return_value = "160"),
_extract_special_attributes_from_ad_page = AsyncMock(return_value = {}),
_extract_pricing_info_from_ad_page = AsyncMock(return_value = (10, "FIXED")),
_extract_shipping_info_from_ad_page = AsyncMock(return_value = ("SHIPPING", 4.99, options)),
_extract_sell_directly_from_ad_page = AsyncMock(return_value = eligible),
_download_images_from_ad_page = AsyncMock(return_value = []),
_extract_contact_from_ad_page = AsyncMock(return_value = ContactPartial()),
), caplog.at_level("WARNING"):
ad_cfg, *_ = await test_extractor._extract_ad_page_info_with_directory_handling(tmp_path, 12345)
assert ad_cfg.sell_directly is expected
assert ad_cfg.shipping_costs == 4.99
assert ad_cfg.content_hash
assert ad_cfg.shipping_options == expected_options
assert ("no predefined shipping option" in caplog.text) is warned
assert ad_cfg.to_ad(test_extractor.config.ad_defaults).sell_directly is expected

@pytest.mark.asyncio
async def test_extract_ad_page_info_uses_css_selector_for_creation_date(
self,
Expand Down
Loading