mirror of
https://github.com/Second-Hand-Friends/kleinanzeigen-bot.git
synced 2026-03-12 02:31:45 +01:00
feat: preview auto price reduction decisions in verify command (#829)
This commit is contained in:
@@ -111,9 +111,9 @@ When `auto_price_reduction.enabled` is set to `true`, the bot lowers the configu
|
||||
|
||||
**Note:** `repost_count` and price reduction counters are only incremented and persisted after a successful publish. Failed publish attempts do not advance the counters.
|
||||
|
||||
When automatic price reduction is enabled, each `publish` run logs a concise decision line that states whether the reduction was applied or skipped and why (repost delay/day delay), including the earliest next eligible reduction condition.
|
||||
When automatic price reduction is enabled, each `publish` run logs one clear INFO message per ad summarizing the outcome—whether the price was reduced, kept, or the reduction was delayed (and why). The `verify` command also previews these outcomes for all configured ads so you can validate your pricing configuration without triggering a publish cycle. Ads without `auto_price_reduction` configured are silently skipped at default log level.
|
||||
|
||||
If you run with `-v` / `--verbose`, the bot additionally logs the full cycle-by-cycle calculation trace (base price, reduction value, rounded step result, and floor clamp).
|
||||
If you run with `-v` / `--verbose`, the bot additionally logs structured decision details (repost counts, cycle state, day delay, reference timestamps) and the full cycle-by-cycle calculation trace (base price, reduction value, rounded step result, and floor clamp).
|
||||
|
||||
```yaml
|
||||
auto_price_reduction:
|
||||
@@ -323,7 +323,7 @@ republication_interval: 7
|
||||
## Troubleshooting
|
||||
|
||||
- **Schema validation errors**: Run `kleinanzeigen-bot verify` (binary) or `pdm run app verify` (source) to see which fields fail validation.
|
||||
- **Price reduction not applying**: Confirm `auto_price_reduction.enabled` is `true`, `min_price` is set, and you are using `publish` (not `update`). Remember ad-level values override `ad_defaults`.
|
||||
- **Price reduction not applying**: Confirm `auto_price_reduction.enabled` is `true`, `min_price` is set, and you are using `publish` (not `update`). Run `kleinanzeigen-bot verify` to preview outcomes, or add `-v` for detailed decision data including repost/day-delay state. Remember ad-level values override `ad_defaults`.
|
||||
- **Shipping configuration issues**: Use `shipping_type: SHIPPING` when setting `shipping_costs` or `shipping_options`, and pick options from a single size group (S/M/L).
|
||||
- **Category not found**: Verify the category name or ID and check any custom mappings in `config.yaml`.
|
||||
- **File naming/prefix mismatch**: Ensure ad files match your `ad_files` glob and prefix (default `ad_`).
|
||||
|
||||
@@ -75,7 +75,7 @@ def _repost_cycle_ready(
|
||||
return False
|
||||
|
||||
if eligible_cycles <= applied_cycles:
|
||||
LOG.debug(
|
||||
LOG.info(
|
||||
"Auto price reduction already applied for [%s]: %s reductions match %s eligible reposts", ad_file_relative, applied_cycles, eligible_cycles
|
||||
)
|
||||
return False
|
||||
@@ -150,6 +150,14 @@ def _day_delay_state(ad_cfg:Ad) -> tuple[bool, int | None, datetime | None]:
|
||||
return elapsed_days >= delay_days, elapsed_days, reference
|
||||
|
||||
|
||||
def _relative_ad_path(ad_file:str, config_file_path:str) -> str:
|
||||
"""Compute an ad file path relative to the config directory, falling back to the absolute path."""
|
||||
try:
|
||||
return str(Path(ad_file).relative_to(Path(config_file_path).parent))
|
||||
except ValueError:
|
||||
return ad_file
|
||||
|
||||
|
||||
def apply_auto_price_reduction(ad_cfg:Ad, _ad_cfg_orig:dict[str, Any], ad_file_relative:str) -> None:
|
||||
"""
|
||||
Apply automatic price reduction to an ad based on repost count and configuration.
|
||||
@@ -162,6 +170,7 @@ def apply_auto_price_reduction(ad_cfg:Ad, _ad_cfg_orig:dict[str, Any], ad_file_r
|
||||
:param ad_file_relative: Relative path to the ad file for logging
|
||||
"""
|
||||
if not ad_cfg.auto_price_reduction.enabled:
|
||||
LOG.debug("Auto price reduction: not configured for [%s]", ad_file_relative)
|
||||
return
|
||||
|
||||
base_price = ad_cfg.price
|
||||
@@ -183,7 +192,7 @@ def apply_auto_price_reduction(ad_cfg:Ad, _ad_cfg_orig:dict[str, Any], ad_file_r
|
||||
|
||||
if not _repost_cycle_ready(ad_cfg, ad_file_relative, repost_state = repost_state):
|
||||
next_repost = delay_reposts + 1 if total_reposts <= delay_reposts else delay_reposts + applied_cycles + 1
|
||||
LOG.info(
|
||||
LOG.debug(
|
||||
"Auto price reduction decision for [%s]: skipped (repost delay). next reduction earliest at repost >= %s and day delay %s/%s days."
|
||||
" repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s",
|
||||
ad_file_relative,
|
||||
@@ -198,7 +207,7 @@ def apply_auto_price_reduction(ad_cfg:Ad, _ad_cfg_orig:dict[str, Any], ad_file_r
|
||||
return
|
||||
|
||||
if not _day_delay_elapsed(ad_cfg, ad_file_relative, day_delay_state = day_delay_state):
|
||||
LOG.info(
|
||||
LOG.debug(
|
||||
"Auto price reduction decision for [%s]: skipped (day delay). next reduction earliest when elapsed_days >= %s."
|
||||
" elapsed_days=%s repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s",
|
||||
ad_file_relative,
|
||||
@@ -211,7 +220,7 @@ def apply_auto_price_reduction(ad_cfg:Ad, _ad_cfg_orig:dict[str, Any], ad_file_r
|
||||
)
|
||||
return
|
||||
|
||||
LOG.info(
|
||||
LOG.debug(
|
||||
"Auto price reduction decision for [%s]: applying now (eligible_cycles=%s, applied_cycles=%s, elapsed_days=%s/%s).",
|
||||
ad_file_relative,
|
||||
eligible_cycles,
|
||||
@@ -383,7 +392,11 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
# Check for updates on startup
|
||||
checker = UpdateChecker(self.config, self._update_check_state_path)
|
||||
checker.check_for_updates()
|
||||
self.load_ads()
|
||||
self.ads_selector = "all"
|
||||
if ads := self.load_ads(exclude_ads_with_id = False):
|
||||
for ad_file, ad_cfg, ad_cfg_orig in ads:
|
||||
ad_file_relative = _relative_ad_path(ad_file, self.config_file_path)
|
||||
apply_auto_price_reduction(ad_cfg, ad_cfg_orig, ad_file_relative)
|
||||
LOG.info("############################################")
|
||||
LOG.info("DONE: No configuration errors found.")
|
||||
LOG.info("############################################")
|
||||
@@ -923,6 +936,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
ensure(images or not ad_cfg.images, f"No images found for given file patterns {ad_cfg.images} at {ad_dir}")
|
||||
ad_cfg.images = list(dict.fromkeys(images))
|
||||
|
||||
LOG.info(" -> LOADED: ad [%s]", ad_file_relative)
|
||||
ads.append((ad_file, ad_cfg, ad_cfg_orig))
|
||||
|
||||
LOG.info("Loaded %s", pluralize("ad", ads))
|
||||
@@ -1552,12 +1566,7 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904
|
||||
|
||||
# Apply auto price reduction only for REPLACE operations (actual reposts)
|
||||
# This ensures price reductions only happen on republish, not on UPDATE
|
||||
try:
|
||||
ad_file_relative = str(Path(ad_file).relative_to(Path(self.config_file_path).parent))
|
||||
except ValueError:
|
||||
# On Windows, relative_to fails when paths are on different drives
|
||||
ad_file_relative = ad_file
|
||||
apply_auto_price_reduction(ad_cfg, ad_cfg_orig, ad_file_relative)
|
||||
apply_auto_price_reduction(ad_cfg, ad_cfg_orig, _relative_ad_path(ad_file, self.config_file_path))
|
||||
|
||||
LOG.info("Publishing ad '%s'...", ad_cfg.title)
|
||||
await self.web_open(f"{self.root_url}/p-anzeige-aufgeben-schritt2.html")
|
||||
|
||||
@@ -60,6 +60,7 @@ kleinanzeigen_bot/__init__.py:
|
||||
" -> SKIPPED: ad [%s] is not in list of given ids.": " -> ÜBERSPRUNGEN: Anzeige [%s] ist nicht in der Liste der angegebenen IDs."
|
||||
" -> SKIPPED: ad [%s] is not new. already has an id assigned.": " -> ÜBERSPRUNGEN: Anzeige [%s] ist nicht neu. Eine ID wurde bereits zugewiesen."
|
||||
"Category [%s] unknown. Using category [%s] with ID [%s] instead.": "Kategorie [%s] unbekannt. Verwende stattdessen Kategorie [%s] mit ID [%s]."
|
||||
" -> LOADED: ad [%s]": " -> GELADEN: Anzeige [%s]"
|
||||
"Loaded %s": "%s geladen"
|
||||
"ad": "Anzeige"
|
||||
|
||||
@@ -164,11 +165,6 @@ kleinanzeigen_bot/__init__.py:
|
||||
apply_auto_price_reduction:
|
||||
"Auto price reduction is enabled for [%s] but no price is configured.": "Automatische Preisreduzierung ist für [%s] aktiviert, aber es wurde kein Preis konfiguriert."
|
||||
"Auto price reduction is enabled for [%s] but min_price equals price (%s) - no reductions will occur.": "Automatische Preisreduzierung ist für [%s] aktiviert, aber min_price entspricht dem Preis (%s) - es werden keine Reduktionen auftreten."
|
||||
? "Auto price reduction decision for [%s]: skipped (repost delay). next reduction earliest at repost >= %s and day delay %s/%s days. repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s"
|
||||
: "Entscheidung automatische Preisreduzierung für [%s]: übersprungen (Repost-Verzögerung). Nächste Reduktion frühestens bei Repost >= %s und Tagesverzögerung %s/%s Tage. repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s"
|
||||
? "Auto price reduction decision for [%s]: skipped (day delay). next reduction earliest when elapsed_days >= %s. elapsed_days=%s repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s"
|
||||
: "Entscheidung automatische Preisreduzierung für [%s]: übersprungen (Tagesverzögerung). Nächste Reduktion frühestens bei elapsed_days >= %s. elapsed_days=%s repost_count=%s eligible_cycles=%s applied_cycles=%s reference=%s"
|
||||
"Auto price reduction decision for [%s]: applying now (eligible_cycles=%s, applied_cycles=%s, elapsed_days=%s/%s).": "Entscheidung automatische Preisreduzierung für [%s]: wird jetzt angewendet (eligible_cycles=%s, applied_cycles=%s, elapsed_days=%s/%s)."
|
||||
"Auto price reduction applied: %s -> %s after %s reduction cycles": "Automatische Preisreduzierung angewendet: %s -> %s nach %s Reduktionszyklen"
|
||||
"Auto price reduction kept price %s after attempting %s reduction cycles": "Automatische Preisreduzierung hat Preis %s beibehalten nach dem Versuch von %s Reduktionszyklen"
|
||||
_repost_cycle_ready:
|
||||
|
||||
@@ -236,3 +236,41 @@ def test_cli_subcommands_with_config_formats(
|
||||
assert result.returncode == 0
|
||||
elif subcommand == "diagnose":
|
||||
assert "browser connection diagnostics" in out or "browser-verbindungsdiagnose" in out, f"Expected diagnostic output for 'diagnose'.\n{out}"
|
||||
|
||||
|
||||
@pytest.mark.smoke
|
||||
def test_verify_shows_auto_price_reduction_decisions(tmp_path:Path, test_bot_config:Config) -> None:
|
||||
"""Smoke: verify command previews auto price reduction decisions for all configured ads."""
|
||||
config_dict = test_bot_config.model_dump()
|
||||
config_dict["ad_files"] = ["./**/ad_*.yaml"]
|
||||
config_path = tmp_path / "config.yaml"
|
||||
yaml = YAML(typ = "unsafe", pure = True)
|
||||
with open(config_path, "w", encoding = "utf-8") as f:
|
||||
yaml.dump(config_dict, f)
|
||||
|
||||
ad_dir = tmp_path / "ads"
|
||||
ad_dir.mkdir()
|
||||
ad_yaml = ad_dir / "ad_test_pricing.yaml"
|
||||
ad_yaml.write_text(
|
||||
"title: Test Auto Pricing Ad\n"
|
||||
"description: A test ad to verify auto price reduction preview\n"
|
||||
"category: 161/gezielt\n"
|
||||
"price: 200\n"
|
||||
"price_type: FIXED\n"
|
||||
"repost_count: 3\n"
|
||||
"auto_price_reduction:\n"
|
||||
" enabled: true\n"
|
||||
" strategy: PERCENTAGE\n"
|
||||
" amount: 10\n"
|
||||
" min_price: 100\n"
|
||||
" delay_reposts: 0\n"
|
||||
" delay_days: 0\n",
|
||||
encoding = "utf-8",
|
||||
)
|
||||
|
||||
args = ["verify", "--config", str(config_path), "--workspace-mode", "portable"]
|
||||
result = invoke_cli(args, cwd = tmp_path)
|
||||
assert result.returncode == 0
|
||||
out = (result.stdout + "\n" + result.stderr).lower()
|
||||
assert "no configuration errors found" in out, f"Expected 'no configuration errors found' in output.\n{out}"
|
||||
assert "auto price reduction applied" in out, f"Expected auto price reduction applied log in output.\n{out}"
|
||||
|
||||
@@ -275,7 +275,7 @@ def test_apply_auto_price_reduction_respects_repost_delay(caplog:pytest.LogCaptu
|
||||
|
||||
ad_orig:dict[str, Any] = {}
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
apply_auto_price_reduction(ad_cfg, ad_orig, "ad_delay.yaml")
|
||||
|
||||
assert ad_cfg.price == 200
|
||||
|
||||
Reference in New Issue
Block a user