Files
kleinanzeigen-bot/docs/AD_CONFIGURATION.md
Jens d384f172b8 fix: remove individual shipping publishing (#1159)
## ℹ️ Description

- Link to the related issue(s): Issue #1157
- Kleinanzeigen no longer offers individual/custom shipping. This
updates publishing, validation, docs, schemas, translations, and tests
so existing `shipping_costs` configs are handled gracefully instead of
failing against removed DOM controls.

## 📋 Changes Summary

- Keep `shipping_costs` for legacy configs/downloaded ads, but mark it
deprecated in schema/docs and ignore it during publishing.
- Remove publishing interaction with removed individual-shipping DOM
controls.
- Require predefined non-empty `shipping_options` for `sell_directly`.
- Update docs, generated schemas/config artifacts, German translations,
and unit tests.
- Verified live DOM for `sell_directly: true` + `shipping_type:
SHIPPING` + predefined `shipping_options` via local ignored script; no
ad submission performed.

### ⚙️ Type of Change
Select the type(s) of change(s) included in this pull request:
- [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
Before requesting a review, confirm the following:
- [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

* **Documentation**
* Updated shipping guides to deprecate custom individual shipping costs
and make publishing depend on per-ad predefined `shipping_options`
(DHL/Hermes; selectable options must come from a single size group).
* Added migration guidance replacing `shipping_costs` with the correct
`shipping_options`.
* Clarified “Sell directly” requires `shipping_type: SHIPPING`,
non-empty `shipping_options`, and `FIXED`/`NEGOTIABLE` pricing.

* **Bug Fixes**
* Publishing is blocked when `shipping_costs` is provided without
`shipping_options`.
* Removed unsupported individual-shipping handling and tightened “Sell
directly” eligibility rules.

* **Tests**
* Expanded unit coverage for deprecation behavior, content handling, and
direct-buy validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-21 08:11:02 +02:00

434 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ad Configuration Reference
Complete reference for ad YAML files in kleinanzeigen-bot.
## File Format
Each ad is described in a separate JSON or YAML file. The bot loads whichever files match the `ad_files` glob in `config.yaml`. Many users keep the default `ad_` prefix for downloaded files (for example, `ad_laptop.yaml`), but the filename itself is user-controlled.
Examples below use YAML, but JSON uses the same keys and structure.
Parameter values specified in the `ad_defaults` section of `config.yaml` don't need to be specified again in the ad configuration file.
## Quick Start
Generate sample ad files using the download command:
```bash
# Download all ads from your profile
kleinanzeigen-bot download --ads=all
# Download only new ads (not locally saved yet)
kleinanzeigen-bot download --ads=new
# Download specific ads by ID
kleinanzeigen-bot download --ads=1,2,3
```
For full JSON schema with IDE autocompletion support, see:
- [schemas/ad.schema.json](https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/ad.schema.json)
Looking for shipping setup? See [Shipping Configuration](#shipping-configuration), including the [Shipping Options Reference](#shipping-options-reference).
📖 **[Complete Main Configuration Reference →](CONFIGURATION.md)**
Full documentation for `config.yaml` including all options, timeouts, browser settings, update checks, and ad_defaults.
## Configuration Structure
### Basic Ad Properties
Description values can be multiline. See <https://yaml-multiline.info/> for YAML syntax examples.
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/main/schemas/ad.schema.json
active: true
type: OFFER
title: "Your Ad Title"
description: |
Your ad description here.
Supports multiple lines.
```
### Description Prefix and Suffix
You can add prefix and suffix text to your ad descriptions in two ways:
#### New Format (Recommended)
In your `config.yaml` file you can specify a `description_prefix` and `description_suffix` under the `ad_defaults` section:
```yaml
ad_defaults:
description_prefix: "Prefix text"
description_suffix: "Suffix text"
```
#### Legacy Format
In your ad configuration file you can specify a `description_prefix` and `description_suffix`:
```yaml
description_prefix: "Prefix text"
description_suffix: "Suffix text"
```
#### Precedence
The ad-level setting has precedence over the `config.yaml` default. If you specify both, the ad-level setting will be used. We recommend using the `config.yaml` defaults as it is more flexible and easier to manage.
### Category
Built-in category name, custom category name from `config.yaml`, or category ID.
```yaml
# Built-in category name (see default list at
# https://github.com/Second-Hand-Friends/kleinanzeigen-bot/blob/main/src/kleinanzeigen_bot/resources/categories.yaml)
category: "Elektronik > Notebooks"
# Custom category name (defined in config.yaml)
category: "Verschenken & Tauschen > Tauschen"
# Category ID
category: 161/278
```
### Price and Price Type
```yaml
price: # Price in euros; decimals allowed but will be rounded to nearest whole euro on processing
# (prefer whole euros for predictability)
price_type: # one of: FIXED, NEGOTIABLE, GIVE_AWAY (default: NEGOTIABLE)
```
### Automatic Price Reduction
When `auto_price_reduction.enabled` is set to `true`, the bot evaluates whether a price reduction is due each time the ad is republished via the `publish` command — the price is only lowered when all eligibility gates (repost cycle, day delay, minimum floor) are satisfied.
**Important:** By default, price reductions only apply when using the `publish` command (which deletes the old ad and creates a new one). Using the `update` command does NOT advance the reduction cycle. Set `on_update: true` (see below) to also apply reductions during update runs. Regardless of the `on_update` setting, the effective reduced price is always restored before submitting an update — this prevents previously applied reductions from being silently lost.
`repost_count` is tracked for every ad (and persisted inside the corresponding `ad_*.yaml`) so reductions continue across runs.
`min_price` is required whenever `enabled` is `true` and must be less than or equal to `price`; this makes an explicit floor (including `0`) mandatory. If `min_price` equals the current price, the bot will log a warning and perform no reduction.
**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` (and optionally `update`) 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 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:
enabled: # true or false to enable automatic price reduction on reposts (default: false)
strategy: # "PERCENTAGE" or "FIXED" (required when enabled is true)
amount: # Reduction amount; interpreted as percent for PERCENTAGE or currency units for FIXED
# (prefer whole euros for predictability)
min_price: # Required when enabled is true; minimum price floor
# (use 0 for no lower bound, prefer whole euros for predictability)
delay_reposts: # Number of reposts to wait before first reduction (default: 0)
delay_days: # Number of days to wait after publication before reductions (default: 0)
on_update: # Also apply price reductions during the update command (default: false)
```
**Note:** All prices are rounded to whole euros after each reduction step.
#### PERCENTAGE Strategy Example
```yaml
price: 150
price_type: FIXED
auto_price_reduction:
enabled: true
strategy: PERCENTAGE
amount: 10
min_price: 90
delay_reposts: 0
delay_days: 0
```
This posts the ad at 150 € the first time, then 135 € (10%), 122 € (10%), 110 € (10%), 99 € (10%), and stops decreasing at 90 €.
**Note:** The bot applies commercial rounding (ROUND_HALF_UP) to full euros after each reduction step. For example, 121.5 rounds to 122, and 109.8 rounds to 110. This step-wise rounding affects the final price progression, especially for percentage-based reductions.
#### FIXED Strategy Example
```yaml
price: 150
price_type: FIXED
auto_price_reduction:
enabled: true
strategy: FIXED
amount: 15
min_price: 90
delay_reposts: 0
delay_days: 0
```
This posts the ad at 150 € the first time, then 135 € (15 €), 120 € (15 €), 105 € (15 €), and stops decreasing at 90 €.
#### Note on `delay_days` Behavior
The `delay_days` parameter counts complete 24-hour periods (whole days) since the ad was published. For example, if `delay_days: 7` and the ad was published 6 days and 23 hours ago, the reduction will not yet apply. This ensures predictable behavior and avoids partial-day ambiguity.
Combined timeline example: with `republication_interval: 3`, `delay_reposts: 1`, and `delay_days: 2`, the first reduction is typically applied on the third publish cycle (around day 8 in a steady schedule, because due ads are republished after more than 3 full days):
- day 0: first publish, no reduction
- day 4: second publish, still waiting for repost delay
- day 8: third publish, first reduction can apply
#### Update-Mode Price Reductions (`on_update`)
By default (`on_update: false`), price reductions only apply during `publish` (full republish). Set `on_update: true` to also apply reductions when running the `update` command (in-place ad modification).
**Update-mode semantics differ from publish-mode in two ways:**
- `delay_days` applies normally — the bot still checks that enough days have elapsed since the last publication before reducing.
- `delay_reposts` is **ignored** — update-mode reductions always evaluate based on the current `price_reduction_count` regardless of `repost_count`, because `update` does not increment `repost_count`.
This means an ad with `delay_reposts: 2` that has only been published once can still receive a price reduction via `update` if `on_update: true` and `delay_days` has elapsed.
#### Restore-First Behavior
When `on_update` is enabled, the reduced effective price is preserved across mixed `publish`/`update` runs:
1. The ad file keeps the configured base `price`.
2. The bot persists `price_reduction_count` after successful runs and recalculates the effective reduced price from `price` + `price_reduction_count` on each run.
3. If you manually edit the `price` field in the ad file, the next run uses the new value as the base for recalculation.
This ensures price reductions accumulate correctly regardless of whether you use `publish`, `update`, or both.
#### Verify Command Preview
The `verify` command previews pricing outcomes for both modes:
- **Publish preview**: Always shown when `auto_price_reduction.enabled` is `true`. Shows the effective price after applying reductions based on the current `repost_count`, `price_reduction_count`, and delay settings.
- **Update preview**: Always shown when `auto_price_reduction.enabled` is `true`. Reports the update-mode outcome: if `on_update` is `true`, it shows the reduction result; if `on_update` is `false` (default), it reports that update-mode reductions are disabled and shows the restored effective price (no new cycle).
```yaml
# Example: enable reductions for both publish and update
auto_price_reduction:
enabled: true
strategy: PERCENTAGE
amount: 10
min_price: 90
delay_reposts: 0
delay_days: 7
on_update: true
```
With this configuration, the price is reduced on every `publish` and also on `update` runs, as long as at least 7 days have passed since the last publication.
Set `auto_price_reduction.enabled: false` (or omit the entire `auto_price_reduction` section) to keep the existing behavior—prices stay fixed and `repost_count` only acts as tracked metadata for future changes.
You can configure `auto_price_reduction` once under `ad_defaults` in `config.yaml`. The `min_price` and `on_update` can be set there or overridden per ad file as needed.
### Special Attributes
Special attributes are category-specific key/value pairs. Use the download command to inspect existing ads in your category and reuse the keys you see under `special_attributes`.
For condition values, use the API values from downloaded ads instead of translated display labels.
Common API values are `new`, `like_new`, `ok`, `alright`, and `defect` (for example API `like_new` corresponds to the German UI label `Sehr Gut`), but available values can vary by category.
```yaml
special_attributes:
# condition_s: like_new # common values include new, like_new, ok, alright, defect
# Example for rental properties
# haus_mieten.zimmer_d: "3" # Number of rooms
```
### Shipping Configuration
> **Important:** Individual/custom shipping (`shipping_costs`) is no longer available on
> kleinanzeigen.de. Only predefined DHL/Hermes carrier options are supported.
```yaml
shipping_type: # one of: PICKUP, SHIPPING, NOT_APPLICABLE (default: SHIPPING)
# DEPRECATED: Individual/custom shipping is no longer supported.
# If shipping_costs is set without shipping_options, publishing is blocked
# with an error. Remove shipping_costs and use predefined shipping_options
# instead (see below). shipping_costs in downloaded ads is still readable
# for backward compatibility.
shipping_costs: # e.g. 2.95 — BLOCKS publishing if set without shipping_options; migrate!
# Specify predefined shipping options / packages.
# Only DHL and Hermes carrier options are valid. You can select multiple
# packages, but only from one size group (S, M, L)!
# See the "Shipping Options Reference" table below for all available options.
shipping_options: [] # non-empty list required for direct-buy (sell_directly)
# Example (size S only):
# shipping_options:
# - DHL_2
# - Hermes_Päckchen
sell_directly: # true or false, requires shipping_type SHIPPING, non-empty predefined shipping_options, and price_type FIXED or NEGOTIABLE (default: false)
```
**Migration from `shipping_costs` to `shipping_options`:**
If your ad YAML currently uses `shipping_costs` (e.g. `shipping_costs: 4.50`) and
`shipping_type: SHIPPING` with no `shipping_options`, replace it with the appropriate
predefined option from the table below. For example, replace:
```yaml
shipping_type: SHIPPING
shipping_costs: 4.50
shipping_options: []
```
with:
```yaml
shipping_type: SHIPPING
shipping_options:
- DHL_2
```
Choose the option that matches your typical package size. If you use multiple carriers,
select multiple options from the same size group.
**`shipping_costs` in downloaded ads:** Extraction still reads `shipping_costs` from
live ads for backward compatibility. However, attempting to publish an ad that has
`shipping_costs` configured without `shipping_options` will **fail with an error**
you must migrate to predefined `shipping_options` before publishing.
#### Shipping Options Reference
You can select multiple options, but **only from one size group** (S, M, or L). Each option corresponds to a specific carrier package. Prices are set by the carrier and may change over time.
| Config Name | Size Group | Carrier | Package | Max Dimensions / Weight |
| --- | --- | --- | --- | --- |
| **Size S (Klein)** | | | | |
| `DHL_2` | S | DHL | DHL Paket 2 kg | 2 kg, max 60 × 30 × 15 cm |
| `Hermes_Päckchen` | S | Hermes | Hermes Päckchen | L+shortest side ≤ 37 cm, max 25 kg |
| `Hermes_S` | S | Hermes | Hermes S-Paket | L+W+H ≤ 50 cm, max 25 kg |
| **Size M (Mittel)** | | | | |
| `DHL_5` | M | DHL | DHL Paket 5 kg | 5 kg |
| `Hermes_M` | M | Hermes | Hermes M-Paket | Standard medium package |
| **Size L (Groß)** | | | | |
| `DHL_10` | L | DHL | DHL Paket 10 kg | 10 kg |
| `DHL_20` | L | DHL | DHL Paket 20 kg | 20 kg |
| `DHL_31,5` | L | DHL | DHL Paket 31,5 kg | 31.5 kg |
| `Hermes_L` | L | Hermes | Hermes L-Paket | Standard large package |
**Shipping types:**
- `PICKUP` - Buyer picks up the item
- `SHIPPING` - Item is shipped. Non-empty `shipping_options` are required for predefined carrier selection and direct-buy (`sell_directly`). Legacy `shipping_costs` is accepted for backward compatibility but ignored during publishing.
- `NOT_APPLICABLE` - Shipping not applicable for this item
**Sell Directly:**
When `sell_directly: true`, buyers can purchase the item directly through the platform without contacting the seller first. This feature requires `shipping_type: SHIPPING` **and** a non-empty list of predefined `shipping_options`. Individual `shipping_costs` alone does **not** qualify for direct-buy. A fixed or negotiable price is also required.
### Images
List of wildcard patterns to select images. If relative paths are specified, they are relative to this ad configuration file.
```yaml
images:
# - laptop_*.{jpg,png}
```
### Contact Information
Contact details for the ad. These override defaults from `config.yaml`.
```yaml
contact:
name:
street:
zipcode:
phone: "" # IMPORTANT: surround phone number with quotes to prevent removal of leading zeros
```
### Republication Interval
How often the ad should be republished (in days). Overrides `ad_defaults.republication_interval` from `config.yaml`.
```yaml
republication_interval: # every X days the ad should be re-published (default: 7)
```
### Auto-Managed Fields
The following fields are automatically managed by the bot. Do not manually edit these unless you know what you're doing.
```yaml
id: # The ID assigned by kleinanzeigen.de
created_on: # ISO timestamp when the ad was first published
updated_on: # ISO timestamp when the ad was last published
content_hash: # Hash of the ad content, used to detect changes
repost_count: # How often the ad has been (re)published; used for automatic price reductions
price_reduction_count: # Auto-managed reduction cycle counter (starts at 0); used with price to recompute effective reduced price
```
`price_reduction_count` is maintained by the bot after successful runs and should not be edited manually.
## Complete Example
```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/Second-Hand-Friends/kleinanzeigen-bot/refs/heads/main/schemas/ad.schema.json
active: true
type: OFFER
title: "Example Ad Title"
description: |
This is a multi-line description.
You can add as much detail as you want here.
The bot will preserve line breaks and formatting.
description_prefix: "For sale: " # Optional ad-level override; defaults can live in config.yaml
description_suffix: " Please message if interested!" # Optional ad-level override
category: "Elektronik > Notebooks"
price: 150
price_type: FIXED
auto_price_reduction:
enabled: true
strategy: PERCENTAGE
amount: 10
min_price: 90
delay_reposts: 0
delay_days: 0
on_update: false
shipping_type: SHIPPING
shipping_options:
- DHL_2
- Hermes_Päckchen
sell_directly: true
images:
- "images/laptop_*.jpg"
contact:
name: "John Doe"
street: "Main Street 123"
zipcode: "12345"
phone: "0123456789"
republication_interval: 7
```
## Best Practices
1. **Use meaningful filenames**: Name your ad files descriptively, e.g., `ad_laptop_hp_15.yaml`
1. **Set defaults in config.yaml**: Put common values in `ad_defaults` to avoid repetition
1. **Test before bulk publishing**: Use `--ads=changed` or `--ads=new` to test changes before republishing all ads
1. **Back up your ad files**: Keep them in version control if you want to track changes
1. **Use price reductions carefully**: Set appropriate `min_price` to avoid underpricing
1. **Check shipping options**: Ensure your shipping options match the actual package size and cost
## 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`, unless `on_update: true`). 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` with non-empty `shipping_options` for predefined DHL/Hermes package shipping and direct-buy. Individual `shipping_costs` is deprecated — publishing an ad with `shipping_costs` and no `shipping_options` will fail with an error. Remove `shipping_costs` from your config and migrate to `shipping_options` (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_`).
- **Image path resolution**: Relative paths are resolved from the ad file location; use absolute paths and check file permissions if images are not found.