```
Title: Add kata ZC1648 to flag `cp /dev/null /var/log/…` and `truncate -s 0 /var/log/…` as audit-log wipes

Description:
Shell scripts sometimes use `cp /dev/null /var/log/auth.log` or `truncate -s 0 /var/log/wtmp` during “cleanup”. This erases audit evidence (failed logins, sudo usage, kernel audit trail) and is almost always a mistake or deliberate cover-up. The proper approach is to rotate logs with `logrotate -f` or vacuum journald with `journalctl --vacuum-time=…`.

We need a new kata with ID **ZC1648** and **severity error** that detects these two dangerous patterns in simple commands.

Acceptance criteria / required behavior:

- **`cp` case:** Trigger when the command name is `cp`, there are at least two arguments, the first argument is the literal string `/dev/null`, and the second argument starts with `/var/log/`. The rest of the arguments are irrelevant for the detection but should be ignored for now.
- **`truncate` case:** Trigger when the command name is `truncate`, the arguments include the option `-s 0` (note: the option and its value appear as two separate tokens `-s` and `0`), and at least one argument is a path that starts with `/var/log/`. The detection should succeed regardless of argument order, as long as both `-s` `0` and a `/var/log/…` path are present.
- **Non-trigger conditions:**
  - `cp /dev/null /tmp/marker` or other destinations not under `/var/log/` must *not* be flagged.
  - `logrotate -f /etc/logrotate.d/app` must *not* be flagged (that’s the safe alternative).
  - `truncate` without `-s 0` or without a `/var/log/` path must be ignored.
- **Violation message:** The message must include the full command line that triggered the rule and clearly state that it wipes an audit log. It should recommend `logrotate -f` or `journalctl --vacuum-time=…` as safe alternatives. The violation’s `Line` and `Column` should come from the command’s token position.
- **Registration:** The kata must be registered for `SimpleCommand` nodes using the existing `RegisterKata` mechanism.

This is the 645th kata in the project, so the `Version` constant in the version package should be bumped to `"0.6.45"` to reflect the new count.

Follow the implementation patterns used by other katas in the `pkg/katas` directory — a single Go file implementing the `Check` function, an `init()` that calls `RegisterKata`, and no changes to the core engine.
```

feat: ZC1648 — flag `cp /dev/null /var/log/` / `truncate -s 0 /var/log/` audit-log wipe (#667)


Feature Request: Add check ZC1273 – detect `grep` with `/dev/null` as a file argument and suggest `grep -q`

**Problem**
Some shell scripts attempt to suppress `grep` output by passing `/dev/null` as a file argument, for example:
`grep pattern file.txt /dev/null`

This does not actually silence output (it adds `/dev/null` as a search file), and it is not idiomatic. The correct and efficient approach is to use `grep -q` (or `--quiet`, `--silent`), which suppresses output and exits on first match.

ZShellCheck should have a kata that catches this pattern and points users toward `grep -q`.

**Expected behavior**
- For a `grep` command where `/dev/null` appears in the argument list and none of the flags `-q`, `--quiet`, `--silent` are present, emit a **style**-level violation.
- The violation message should explain that `grep -q` is faster and more idiomatic than using `/dev/null` as an argument.
- If `grep` already has `-q` / `--quiet` / `--silent`, do **not** flag, even if `/dev/null` is also present.

**Impact**
This would help users adopt a best practice that is common in shell scripting and improve both correctness and performance of their Zsh scripts and plugins. The rule fits with other existing style katas that encourage idiomatic Zsh constructs.

feat: Add ZC1273 — use grep -q instead of redirecting to /dev/null (#300)


```
Title: New kata ZC1435: warn on `killall -9` / `killall -KILL`

Background:
`killall -9 name` (or `killall -KILL name`) sends SIGKILL to *every* process whose name matches, not just the ones owned by the current user. In multi-user, containerised, or CI environments this can terminate unrelated processes that happen to share the same process name, causing unpredictable damage. We currently have no check for this dangerous pattern.

Request:
Add a new kata (ID: ZC1435, severity: warning) that fires when the shell command `killall` is called with the flags `-9` or `-KILL`.

Behaviour:
- The check must scan simple commands (i.e. parsed `SimpleCommand` nodes).
- Trigger only when the command name is exactly `killall`.
- Fire if any argument is exactly `-9` or exactly `-KILL`.
- The violation message should explain why this is dangerous (force-killing all matching processes, risk on multi-user/containerised hosts) and suggest safer alternatives: starting with `-TERM` for a graceful shutdown, or locating the process by PID via `pgrep`/`pidof` before sending a signal.
- The violation should be reported at the line/column of the `killall` command token itself, not the argument.

Project convention:
- Follow the existing kata registration pattern (via `RegisterKata`) using `ast.SimpleCommandNode`.
- Add the corresponding test file under the `katatests` directory, using the project’s test utilities to verify the check.
- Bump the kata count in `pkg/version/version.go` (the version string is formatted as `0.<major>.<kata_count>`).

Edge cases / non‑triggers:
- `killall` without `-9`/`-KILL` (e.g. `killall myproc`) should not trigger.
- `killall -TERM myproc` or other signals should not trigger.
- Do **not** attempt to detect `-s KILL` or combined flag forms (`-9KILL`, etc.) – only the simple `-9` and `-KILL` arguments are in scope for this kata.
```

feat: ZC1435 — warn on `killall -9` (mass force-kill by process name) (#452)


We need a new static‑analysis rule `ZC1768` that flags insecure invocations of the SQL Server CLI tools (`sqlcmd`, `bcp`, `osql`) when a password is passed directly as a command‑line argument via the `-P` flag.

**Observable behaviour / acceptance criteria:**

1. **Trigger command**: The rule applies to any simple command whose name is one of `sqlcmd`, `bcp`, or `osql`.
2. **Flag‑and‑value detection**: If the command’s arguments contain the string `-P` **immediately followed by a non‑empty, non‑`-` argument that does not start with a dash**, then the rule must report exactly one violation.
   * Example triggers: `sqlcmd -P hunter2`, `bcp ... -P 'Secret!'`.
   * Example non‑triggers (bare / prompt style):
     `sqlcmd -P` (no value after `-P`),
     `sqlcmd -P -U user` (the next argument is another option),
     `sqlcmd -P-` (argument is exactly `-`),
     `sqlcmd -P ''` (empty argument).
3. **Violation severity**: error (security issue – password visible in process lists, logs, shell history, and SQL Server audit).
4. **Violation message**: Must mention the command name and the captured password value, explain the exposure (argv, `ps`, `/proc`, history, SQL Server audit), and recommend using bare `-P` (prompt) or the `SQLCMDPASSWORD` environment variable.
5. **Scope**: The check must only look at the arguments of the simple command; it should not descend into sub‑commands, pipes, or other constructs unless they produce their own simple‑command nodes.
6. **Side effects**: Adding this rule must automatically increase the total kata count (the version constant) by one, since the project derives its version from the number of implemented katas.

Please implement the new kata in the usual way (register it for simple‑command nodes in the `pkg/katas` package) and ensure the version bump reflects the new count.

feat: ZC1768 — flag `sqlcmd/bcp/osql -P PASSWORD` (SQL Server pass in argv) (#787)


**Title:** Missing lint for `unsetopt BARE_GLOB_QUAL` / `setopt NO_BARE_GLOB_QUAL`

**Description:**
Zshellcheck currently has no kata that warns when a script disables the `BARE_GLOB_QUAL` Zsh option. This option is **on by default** and is responsible for the Zsh-specific per-glob qualifier syntax (`*(N)`, `*(.x)`, `*(Om)`, …). When it is turned off, Zsh falls back to a ksh‑style interpretation where `(…)` inside a glob becomes a pattern **alternation** – `*(N)` is then no longer a null‑glob but “match zero-or-one `N`”. That silently alters the meaning of existing patterns and can cause null‑glob idioms to fail (especially when `NOMATCH` is active).

**Request:**
Add a new kata (e.g. `ZC1893`) with severity **warning** that detects when `BARE_GLOB_QUAL` is explicitly disabled. The check should cover:

* `unsetopt BARE_GLOB_QUAL`
* `setopt NO_BARE_GLOB_QUAL`

Case and underscores should be normalised so that `no_bare_glob_qual`, `NOBAREGLOBQUAL`, etc. are all recognised. The kata **must not** fire for `setopt BARE_GLOB_QUAL` (explicitly setting the default is harmless) or for unrelated options such as `NOMATCH`.

The message should remind the user that this disables `*(qualifier)` syntax, breaks null‑glob behaviour, and recommend scoping any local changes with `LOCAL_OPTIONS` inside a function instead.

There are already similar katas in the codebase that guard against dangerous option toggles (see `ZC1885` for `CSH_NULL_GLOB`); the implementation can follow the same pattern.

The new kata would bring the total count to **890**, which implies a minor version bump to `0.8.90`.

**Expected behaviour:**
Given a file containing:
```zsh
unsetopt BARE_GLOB_QUAL
```
`zshellcheck` should emit something like:
> `unsetopt BARE_GLOB_QUAL` disables `*(qualifier)` syntax — `*(N)` stops being null‑glob and becomes an alternation, so null‑glob idioms silently break. Keep the option on; scope inside a `LOCAL_OPTIONS` function.

Similarly for `setopt NO_BARE_GLOB_QUAL`.

feat: ZC1893 — warn on `unsetopt BARE_GLOB_QUAL` breaking `*(N)` null-glob qualifier (#914)


**Title:** Add ZC1404: warn on `$BASH_CMDS` usage – suggest Zsh `$commands`

**Background**
Bash provides `$BASH_CMDS`, an associative array mirroring the internal command hash table (name → full path). In Zsh, this variable is not set; users must instead load the `zsh/parameter` module and use `$commands` (also an associative array). Unawareness of this difference leads to silently empty expansions or scripts that don’t work as expected.

**Request**
Implement a new kata, `ZC1404`, that:

- Flags when a Zsh script or function references the Bash-only parameter `$BASH_CMDS`.
- Suggests the replacement: `$commands` associative array (names → paths), available after `zmodload zsh/parameter`.
- Severity: `warning` (risky pattern that will cause incorrect behaviour in Zsh).

**Functional details**
- The check should look at simple commands (e.g., `echo`, `print`, `printf`) where the variable is used as an argument. Example that must trigger:
  `echo $BASH_CMDS` → warning.
- Valid Zsh-native usage such as `echo $commands` should **not** be flagged.
- If a command contains multiple arguments with the offending token, emitting a single violation for the command is acceptable.
- The violation should point to the command’s line/column and include a human-readable message explaining the Zsh alternative.

**Integration**
- Follow the existing kata registration pattern (`RegisterKata` with the appropriate node type from `pkg/ast`).
- Update the kata count in the version string (the convention is that the number of katas determines the version).

feat: ZC1404 — warn on $BASH_CMDS (use Zsh $commands) (#418)


Add a kata rule that warns when `dnf`, `yum`, or `microdnf` is used with one of the subcommands `install`, `upgrade`, `update`, `remove` and the `-y` (or `--assumeyes`) flag is missing. These package-manager invocations can hang in non-interactive scripts waiting for confirmation, so the rule should fire for any such command that lacks the flag. Severity: warning.

feat: ZC1449 — warn on `dnf/yum install` without `-y` (hangs in scripts) (#466)


**Title:** Add kata ZC1644: flag `unzip -P` / `zip -P` password exposure

**Background**
Both `unzip` and `zip` support a `-P` flag that supplies an archive password on the command line. The password ends up in `argv` and is therefore visible to any local user who can list processes (`ps`, `/proc/<pid>/cmdline`), as well as in shell history and audit trails. This is a well-known security anti-pattern.

**Request**
Add a new kata (`ZC1644`) that reports an **error** severity violation whenever `unzip` or `zip` is called with the `-P` flag. The violation message should explain the risk and recommend safer alternatives (interactive prompt, `7z -p` piped via stdin, or envelope encryption with `age`/`gpg` using a protected key file).

**Detection rules**
- Target only simple commands whose command name is exactly `unzip` or `zip`.
- The flag appears in the command’s argument list in two possible forms:
  1. A separate argument that is exactly `-P`, immediately followed by another argument (the password).
  2. A single argument that starts with `-P` and has at least one character after it (e.g., `-Psecret`).
- Both forms must be flagged; both are equally dangerous.
- If neither form is present, the command is considered safe and should not trigger the kata.

**Expected behavior**
- Input: `unzip -P s3cret archive.zip` → single violation with line/column from the command token.
- Input: `zip -Ps3cret archive.zip files/` → single violation with line/column from the command token.
- Input: `unzip archive.zip` or `zip archive.zip files/` → no violations.

**Additional notes**
- The kata should follow the existing registration pattern for `SimpleCommandNode` checks.
- The violation’s `KataID` must be `"ZC1644"` and its `Level` must be `SeverityError`.
- The message must mention the exact command name that triggered the check (i.e., `unzip` or `zip`).

feat: ZC1644 — flag `unzip -P` / `zip -P` archive password in argv (#663)


**Feature Request: Detect destructive `crictl` node‑wide image/container removal**

**Observed behavior**
ZShellCheck issues no warnings for `crictl rmi -a` or `crictl rm -af`. These commands talk directly to the container runtime (containerd/CRI‑O) below the kubelet and silently delete *every* image or *every* container on the node, even those backing running pods. This causes immediate pod failures, forces kubelet to re‑pull images (risking registry rate‑limits), and bypasses PreStop hooks and PodDisruptionBudgets.

**Expected behavior**
ZShellCheck should flag these commands as errors. Specifically:

- `crictl rmi` with any form of the “all” flag (`-a`, `--all`) triggers a violation.
- `crictl rm` with both “all” and “force” flags (any order, also combined as `-af` or `-fa`, or long `--all` / `--force`) triggers a violation.
- Safe invocations (e.g., `crictl rmi $IMAGE_ID`, `crictl ps`, `crictl rm $CONTAINER_ID`) must not be flagged.

A clear error message should explain the risk and point the user toward safe cluster‑level operations (e.g., `kubectl drain` / `kubectl delete pod`).

**Impact**
Accidental execution of these commands on a production node causes widespread workload disruption and can make the node Unready. Adding a dedicated ZShellCheck rule (severity **error**) would prevent operators and CI pipelines from landing scripts containing these destructive patterns.

feat: ZC2002 — detect `crictl rmi -a`/`rm -af` node-level CRI wipe (#1024)


**Subject:** Add auto‑fix for ZC1055 (empty‑string comparisons in `[[ … ]]`)

The ZC1055 style check currently only reports that `[[ $x == "" ]]` should be `[[ -z $x ]]` (and `!= ""` → `-n`). Implement an automatic fix so the linter can rewrite these expressions when run with `--fix`.

**Transformation rules**
- `[[ $var == "" ]]` → `[[ -z $var ]]`
- `[[ $var != "" ]]` → `[[ -n $var ]]`
- Both empty‑string positions must be handled:
  `[[ "" == $var ]]` → `[[ -z $var ]]`   (and `!=` accordingly)
- The empty literal may be `""` or `''`.

**Span / correctness**
- Replace the entire infix expression (`$var == ""`) including the operator, not just the operator.
- Compute the correct byte span from the AST node so the edit covers the variable, operator, and empty literal in source order.
- The variable may be a bare `$x` or something like `${x}` or `$x.ext`.  Use a helper that scans from the token offset to the next delimiter (whitespace, `;`, `|`, `&`, `)`, `]`, `}`) so the replaced text preserves the exact variable expression.
- The edit’s line/column must be accurate; reuse existing helpers (`LineColToByteOffset` etc.) where possible.

**Registration**
Wire the new `Fix` function into the kata’s `init()` registration so the auto‑fixer picks it up.

feat(zc1055): add Fix for empty-string comparisons in brackets (#1170)


**Title:** [Feature] Style check: prefer Zsh `*(f:mode:)` glob qualifier over `find -perm`

**Description**
Zsh’s `*(f:mode:)` glob qualifier is a built-in, idiomatic way to match files by permission mode (octal, e.g. `*(f:0755:)`, or symbolic, e.g. `*(f:u+x:)`). Many scripts still use `find … -perm …`, which spawns an external process and is less native to Zsh. We should add a style-level check that suggests the Zsh-native alternative.

**Expected behavior**
- The check fires only when the command name is `find` and **any** argument is exactly `-perm` (regardless of the permission value that follows, or whether the flag appears combined with other options).
- When `find` is invoked **without** `-perm` the check must not report anything.
- Severity: `style` (following the convention for style suggestions in the project).
- The violation message should clearly mention the `*(f:mode:)` glob qualifier and briefly note that both octal and symbolic expressions are supported.
- The violation location (line/column) should refer to the start of the `find` command, consistent with how other katas report command-level issues.
- The check must integrate with the existing kata registration mechanism and include automated tests as described in the contributor guide.

**Impact**
Encourages Zsh-idiomatic code, avoids unnecessary external process forking, and improves script performance and cleanliness.

feat: ZC1345 — use Zsh `*(f:mode:)` glob qualifier instead of `find -perm` (#358)


Add a new kata `ZC1547` (severity: `warning`) that flags dangerous use of `kubectl apply --prune --all` (and equivalent forms). The kata should fire when the command being checked is `kubectl` or `oc`, the first positional argument is `apply`, the flag `--prune` is present, and at least one of `--all`, `-A`, or `--all-namespaces` appears. The violation message must explain that this combination deletes every cluster resource that matches labels but is missing from the manifest (so a manifest typo or partial deploy can wipe resources owned by other teams). The suggestion should be to scope pruning with a narrow `-l <selector>`.

Follow the Kata registration and check-function pattern used by existing katas (e.g., `ZC1546`). The check should operate on a `SimpleCommand` AST node and extract the command name and arguments to detect the pattern described. Wire the kata into the registry so that the tool can surface it when the new version is built.

feat: ZC1547 — warn on kubectl apply --prune --all (deletes untracked resources) (#565)


**Title:** Add kata for unpinned container image references in docker/podman/nerdctl commands

**User story**
As a developer writing Zsh scripts that use container runtimes, I want ZShellCheck to alert me when a `pull` or `run` command references an image without a pinned version or digest, because the implicit `:latest` tag breaks reproducibility and introduces unnecessary supply‑chain risk every time the script runs.

**Acceptance criteria**
1. **Scope** – The check applies to the following commands:
   - `docker pull <image>`
   - `docker run <image> ...`
   - `podman pull <image>`
   - `podman run <image> ...`
   - `nerdctl pull <image>`
   - `nerdctl run <image> ...`

2. **Violations** – A `style`‑level violation must be reported when:
   - The image reference has **no tag and no digest** (e.g. `nginx`).
   - The image reference uses the **explicit `:latest` tag** (e.g. `nginx:latest`).
   The violation message should clearly say the image is unpinned (implicit `:latest`) and recommend pinning to a specific tag or an `@sha256:`/`@sha512:` digest.

3. **No violation** must be reported when:
   - The image reference has a **specific tag** other than `latest` (e.g. `nginx:1.27`, `my.registry.io/app:v2`).
   - The image reference includes an **immutable digest** (`@sha256:...` or `@sha512:...`).
   - The command is something other than `pull` or `run` (e.g. `docker build`).
   - The executable name is not `docker`, `podman`, or `nerdctl`.

4. **Flags and options** – Arguments that start with `-` or contain `=` should be skipped when searching for the image argument.
   For `docker run`, the image is typically the first non‑flag, non‑option argument after the sub‑command; the check should handle this correctly.

5. **Registry port edge case** – If the image reference contains a colon that is part of a registry port (e.g. `localhost:5000/myimage`), that colon must **not** be interpreted as a tag delimiter. Such a reference without a subsequent tag/digest should still be flagged as unpinned.

6. **Integration** – The rule must be registered in the kata system so that it is applied automatically when ZShellCheck processes a Zsh script. Its severity must be `style`.

7. **Version impact** – Adding this kata increases the total number of implemented katas; the method that calculates the version from the kata count must reflect the new total.

feat: ZC1499 — style: docker pull <image> without tag / :latest (unpinned image) (#516)


**Bug:** `getListItemsDeleted()` always returns all deleted list items, no per-domain filtering

**Observed behavior**
The `getListItemsDeleted()` method (both in the `MShop\Common\Item\ListRef\Iface` interface and its `Traits` implementation) returns every list item that has been deleted, regardless of domain. When code only cares about deletions from one domain (e.g., `'text'`), it must manually filter the returned array.

**Expected behavior**
The method should accept an optional domain string.
- If a domain is provided, return only the deleted list items belonging to that domain.
- If no domain is given, return all deleted items (backwards compatible).
- When a domain is requested but no items were deleted for that domain, return an empty array.

This matches the pattern already used by `getListItems()` and `getRefItems()`, which both offer optional domain filtering.

**Impact**
Callers that process domain-specific deletions (for example, during save/commit) currently must iterate and filter themselves. Providing a direct filtered retrieval would simplify those callers and reduce the chance of accidentally mixing domains.

Added optional domain to getListItemsDeleted()


## Bug Report: Product CSV import does not correctly replace existing product references

### Problem
When importing product-to-product references via the product CSV processor, the importer attempts to reuse existing list items based on their positional order inside the internal stale-item pool.  This leads to incorrect assignments and stale references when the order of rows in the CSV changes, when codes are omitted from a previously imported set, or when the CSV contains multiple codes per row.

### Expected Behaviour
After processing a CSV import for a product, the product’s list items for the allowed list types **must exactly match** what is declared in the CSV data:

- For each distinct `product.lists.type` that appears in the CSV rows, **all** existing list items of that type for the product are replaced by the set of references derived from the CSV data.
- For any list type that is part of the configured list-type filter but **not present** in the CSV data for the product, **all** existing list items of that type are removed from the product.
- If the CSV data for a product contains **no reference rows at all**, every existing list item belonging to the filtered types is deleted from the product.
- Multiple product codes per type (separated by the configurable import separator, typically a newline) must each generate a separate list item under that type.

The import must be **idempotent**: re-importing the same CSV data twice must produce the same final list item state, with no artefacts from earlier imports or positional reuse.

### Suggested Approach
The importer should delegate the bulk update to the product manager’s `updateListItems()` method (passing a map of product codes keyed by their resolved product IDs per type) instead of manually creating, populating, and saving individual list items from a stale-item pool.  This avoids fragile positional matching and ensures the final list exactly matches the CSV input.

Use updateListitems() for referencing products in product CSV import


**Request: Restrict product downloads to a single file per product**

*Background:* The product download administration currently allows attaching multiple download files to a product. However, the order system cannot store more than one hidden product attribute with the same type and code for a given product. Because of this constraint, multiple downloads per product are not actually supported.

*Acceptance criteria:*
1. The product download section in the administration interface must allow only one downloadable file to be associated with a product.
2. The UI should display a single set of fields (file upload, status, name) instead of a multi-row table with add/remove controls.
3. When saving, exactly one hidden attribute of type “download” is stored for the product. The backend must not create or persist multiple download attributes.
4. Existing products that already have multiple download attributes should be handled gracefully: display and update only the first download attribute; extraneous attributes should be ignored (and cleaned up if feasible, but at minimum must not interfere with the single-record save).
5. The section heading should change from “Downloads” to “Download” (singular).

*Non-goal:* This request does not change how downloads are handled in the storefront or order processing beyond the described restriction; it only enforces the single-download rule in the administration UI and persistence.


When a function passed to `Aimeos\MW\Process\Pcntl::start()` encounters an uncaught throwable (an `Exception` or a fatal error such as a `TypeError`), the parent process should log the failure to `STDERR` **without throwing an exception from `wait()`**. The parent must remain alive to wait for any other pending child processes, as long as the maximum number of parallel processes allows. If the `$restart` flag is true, the process should be respawned; otherwise a message containing the PID and exit status should be written to `STDERR`. The error output from the child (exception message and trace) must also go to `STDERR`, consistently with the way the child writes normal exit messages.

Acceptance criteria:
- Child processes that throw any `\Throwable` (PHP ≥7.0) or `\Exception` (PHP 5.x) must **not** cause the parent to terminate or to throw a `Process\Exception` from `wait()`.
- Both the child’s error details and the parent’s failure notice (PID + status) must appear on `STDERR`.
- The parent must continue waiting for other child processes after one child fails.
- The `$restart` mechanism must still work: if the child was started with `$restart = true`, the function should be re-invoked in a new child instead of logging the failure.
- The fix must not break the existing behavior for successful child processes.

Log error if child process dies but continue


**Feature: Session-based caching for “last seen” rendered fragments**

The catalog session seen client (`Client_Html_Catalog_Session_Seen_Default`) currently recomputes its body and header on every request, even when the set of last-seen products hasn’t changed. Add session-scoped caching of the rendered HTML to eliminate this redundant work.

**Key requirements:**

1. **Cache in getBody/getHeader**
   Before building the view and rendering the template, check the session for a previously stored fragment. If a cached value exists, return it directly. Otherwise, compute, render, and store the result in the session so it is available on subsequent requests.

2. **Safe cache keys**
   Use a unique key per output fragment (body/header) derived from the method’s parameters and configuration, so that different configurations produce independent cache entries. Store the key in a session-tracked set so all cached entries can be enumerated later.

3. **Invalidation**
   The catalog detail seen client (`Client_Html_Catalog_Detail_Seen_Default`) already manages the list of seen product IDs during `process()`. After the list is updated, immediately clear every cached fragment that was stored by the session seen client (using the tracked set of keys). This ensures the next call to getBody/getHeader will regenerate with the fresh list.

4. **Consistent session key for the list**
   Both clients currently read/write the seen-item list under `arcavias/client/html/catalog/session/seen`. Change this to a shorter, consistent key (e.g., one under `arcavias/catalog/session/seen/list`) so the list storage and the cache management keys follow a coherent naming scheme.

5. **No stale data**
   When the list changes, no remnants of previously cached fragments should survive. The invalidation step must guarantee that all keys previously recorded in the tracking structure are cleared from the session.

The modified tests should still pass after adjusting the session key references accordingly.

Implements session based caching for the last seen items


**Feature request: Translate view helper — detect missing translations**

The Translate view helper (`transform()` on `Aimeos\MW\View\Helper\Translate\Iface`) currently always returns a string: the translated text when available, or the original text as a fallback when no translation exists. There is no way for a caller to distinguish these two cases.

We need an optional `$force` parameter (boolean, default `true`) on the `transform()` method. When `$force` is `false`, the method must return `null` if no translation is available, instead of the fallback string. When `$force` is `true` (or omitted), the existing behaviour must be preserved exactly — backward compatibility is essential.

Additionally, the `$plural` parameter should accept `null` as a value (currently it expects a non‑empty string), to accommodate callers that want to pass `null` explicitly, especially when they are using the new nullable‑return behaviour.

**Acceptance criteria**

1. When `$force` is `false` **and** the underlying translator returns a string identical to the input singular form (no translation was found for a singular key), `transform()` returns `null`.
2. When `$force` is `false` **and** the underlying translator returns a string identical to the input plural form (no translation was found for a plural key), `transform()` returns `null`.
3. When a genuine translation exists (the translator returns a string different from the input singular or plural), `transform()` returns that translated string, regardless of the `$force` flag.
4. When `$force` is `true` or not passed, `transform()` always returns a string — either the translated text or the original (singular/plural) fallback.
5. The `$plural` parameter must work correctly when passed as `null` (treating `null` as “no plural form provided”).
6. Existing code that calls `transform()` without the new parameter must continue to work unchanged.

**Implementation notes**

- The behaviour change must be reflected in both the interface (`Iface`) and the default implementation (`Standard`).
- A “missing translation” is signalled by the translator returning the same string that was given as the source text (singular or plural). Use this property to decide whether to return `null` when `$force` is `false`.
- The default value for `$plural` should become `null` so that `null` is a valid argument. Ensure the logic for choosing singular vs. plural path still works correctly with a `null` plural.

Translate view helper can now return NULL if no translation is available


We need to add radius-based supplier proximity search to the product index.

### Background
Currently the `mshop_index_supplier` table only stores product-supplier associations (product ID, supplier ID, list type, position). Suppliers can have addresses with latitude/longitude fields, but those coordinates are not indexed. This prevents filtering products by supplier distance from a given location, which is needed for e.g. "find products available from nearby suppliers".

### Expected behavior
- **Index schema**: Extend the `mshop_index_supplier` table with nullable latitude and longitude columns (float). An index over (product ID, latitude, longitude, site ID) should be added for efficient querying.
- **Index rebuild**: When the supplier index is rebuilt (the `rebuild` method in the supplier index manager), the latitude and longitude should be populated for each product-supplier entry from the associated supplier's address(es). If a supplier has multiple addresses, each address should generate a separate index row with its coordinates. If a supplier has no address coordinates, latitude and longitude should be NULL.
- **Search capability**: A new search attribute named `index.supplier:radius` should be available on the supplier index manager. It must accept three parameters: latitude, longitude, and distance in kilometers. The condition should match products whose associated supplier has at least one address within the given radius (Haversine-based or equivalent bounding-box pre-filter). The attribute type should be boolean (products match or don't).
- **Site isolation**: The existing site ID filtering must still be applied in the radius condition.
- **Backward compatibility**: The existing `index.supplier:position` searches and other index functionality must continue to work unchanged. The rebuild must still populate all existing columns correctly.
- **Test data**: The supplier test data already contains latitude/longitude values but may need adjustment (e.g., field order) to ensure correct mapping when the index is rebuilt. Address items' coordinate getters must work.

Please implement the feature following this specification. Do not modify unrelated subsystems; changes should be localized to the supplier index manager, its configuration, schema migration, and related test data where necessary.

Implemented radius search for products suppliers


**Request: Support filesystem name in imageset view helper**

The `imageset` view helper is used to generate an `srcset` attribute value from a map of image widths to paths. Internally, it delegates to the `content` view helper to resolve each path to a full URL. The `content` view helper’s `transform` method accepts an optional filesystem name parameter (defaulting to `'fs-media'`) so that callers can choose which configured filesystem base URL to use.

Currently, the `imageset` helper’s `transform` method does not expose this parameter. It always hardcodes `'fs-media'` when calling `content()`. This prevents use of alternative filesystem configurations (for example, when images are served from a different storage backend identified by a different resource key).

**Acceptance criteria**

1. The `transform` method of the `imageset` view helper must accept an optional filesystem name argument (string, default `'fs-media'`) as its second parameter.
2. When constructing each URL in the srcset, that filesystem name must be forwarded to the `content` view helper call, so that URL resolution uses the correct configuration block.
3. The existing behavior—calling `transform` with only the images array—must remain unchanged. Existing callers that do not pass a filesystem name must still get the same results (using `'fs-media'`).
4. The feature must work correctly when an alternative filesystem name is provided and a matching resource configuration exists (e.g., a `baseurl` defined under `resource.<fsname>`).

**Non-functional constraints**

- Do not change the behavior of the `content` view helper; it already supports the parameter.
- Maintain the existing return type (`string`) and join format (comma-separated `url widthw` entries).
- No other public method signatures should change.


We should harmonize the `getListItem()` API with the rest of the `ListRef` interface.

Currently, the `ListRef\Base` class has a `getListItem()` method, but the `ListRef\Iface` interface does not declare it. Additionally, the parameter order (`refId, domain, listtype`) is inconsistent with `getListItems()` and `getRefItems()`, which both use `domain` as the first parameter.

**Requested changes:**

1. Add a `getListItem()` declaration to the `ListRef\Iface` interface with the signature `( $domain, $listtype, $refId, $active = true )`, including proper docblock.
2. Update the existing implementation in `ListRef\Base` to match the new parameter order and interface declaration.
3. Update the unit test that exercises `getListItem()` so that the call uses the new parameter order.

Make sure the internal logic of `getListItem()` (looking up list items by domain and then matching by list type and reference ID) remains correct after the parameter reorder.

Reordered parameters and added getListItem() to ListRef interface


Add a `getFilesystem()` convenience method to the context item.

The context already provides `getFilesystemManager()`, but callers must then manually call `->get($resource)` on the manager to obtain a filesystem instance. They need the same direct access to named filesystem resources that `getMessageQueue($resource, $queue)` already provides for message queues.

Required contract:
- The method must accept a resource name string (e.g., `'fs-secure'`, `'fs-admin'`).
- It must delegate to the same logic used by `getFilesystemManager()->get($resource)`.
- If no filesystem manager has been set on the context (i.e., the internal manager is unavailable), it must throw an `\Aimeos\MShop\Exception` — consistent with the behavior of other context getters when their backing manager is missing.

Added getFilesystem() method to context item


```markdown
### Feature Request: Media title translations via property type "name"

**Background**
Media items can have property items storing translatable values. Currently the standard property type for a media title is `"title"`, but it should be `"name"` to align with conventions used elsewhere (e.g. product/service/supplier name translations). The `getName()` method on a Media item should use those property values when available, falling back to the existing internal label for backwards compatibility.

**Acceptance Criteria**
1. The default seed data for `media/property/type` must define a type with `code = "name"` instead of `"title"`. The label can stay "Media title".
2. `MShop_Media_Item_Standard::getName()` must check for a property item of the given type (defaulting to `"name"`).
   - If such a property item exists (first one found), return its value.
   - If no matching property item exists, delegate to the parent implementation (which returns the label field), preserving existing behavior.
3. The method signature should remain `getName( $type = 'name' )` – consumers can pass a different type if needed, and the absence of a property of that type results in the standard fallback to the label.
4. Existing tests must continue to pass, and the new behavior should be covered: one test for the default case (no property item, returns label), one test where a property of type `"name"` is added and `getName()` returns that property’s value.

**Non-Goals**
- No changes to other item types or their `getName()` implementations.
- No changes to property handling logic outside the Media item.
- No database migration script (new default type will be picked up on setup updates).
```

Media properties of type "name" are used for title translations


**Feature Request: Simplify customer payment address construction**

The `Customer` item’s payment address is currently built by manually copying each field from the customer’s data in a large switch statement inside the abstract customer base item. This is fragile and error‑prone – every time the address structure changes, the mapping must be kept in sync.

**Acceptance criteria**

1. The payment address (obtained via `getPaymentAddress()`) must still contain all standard address fields: company, VAT ID, salutation, title, first name, last name, address lines 1‑3, postal code, city, state, country ID, language ID, telephone, telefax, website, email, longitude, and latitude.
2. The construction of the payment address should leverage the existing common address base class’s ability to read fields from a key/value array using a prefix (e.g., `'customer.'`). No manual field‑by‑field mapping should remain in the customer item.
3. The common address base class is abstract; provide a minimal concrete implementation that extends it and implements the address interface, just enough to hold the data without any additional persistence logic.
4. The customer manager should use this lightweight address implementation when creating customer items, passing the customer’s own values array directly. It must no longer depend on the address sub‑manager for building the payment address.
5. The external API of customer items remains unchanged – `getPaymentAddress()`, `setPaymentAddress()`, `toArray()` etc. must continue to work as before.

Simplified customer payment address handling


Implement `addRefItem`, `deleteRefItem`, and `getDeletedItems` on items that support list and referenced sub‑items.

Currently, the common `ListRef` item (and its interface) only allow reading list and referenced items via `getListItems`/`getRefItems`. We need mutation methods so that callers can build and modify associations in memory:

- `addRefItem($domain, $listItem, $refItem = null)`: adds the given list item to the specified domain, optionally attaching a referenced item.
  - The list item and the referenced item may or may not have persistent IDs. If an ID is missing, the implementation must generate a temporary key that keeps the new item distinct from existing ones.
  - After the call, the list item should appear in `getListItems($domain)` and the referenced item (if provided) in `getRefItems($domain)`, with the linkage correctly set.

- `deleteRefItem($domain, $listItem, $refItem = null)`: removes an existing list item (identified by object identity) from the domain.
  - The removed list item, with the optional referenced item attached, should be recorded for later retrieval (the underlying manager will need to persist the deletion).
  - If no matching list item is found in the domain’s collection, an exception must be thrown.

- `getDeletedItems()`: returns all list items that have been removed via `deleteRefItem`. Each returned list item may have its referenced item already attached.

The existing read methods (`getListItems`, `getRefItems`) must continue to work correctly after additions and deletions, reflecting the current in‑memory state.

Implemnts methods to add and remove list and referenced items


**Title**: Product references should load sub-items but must not recurse into further product references

**Description**
When `MShop_Product_Manager` loads an item via `findItem()` or `getItem()` with a `$ref` array that includes `'product'`, the referenced product items do not receive any of their own referenced sub-items (attributes, texts, prices, etc.). The base list–reference loading logic builds referenced items without forwarding the requested domains, so nested object graphs remain shallow.

We need the loading to be “deep” enough that one level of product references can carry their non‑product domain items, while avoiding circular dependencies: product ⇒ product ⇒ product ⇒ … must never happen.

**Acceptance criteria / desired behaviour**

1. **Product domain depth limit – one level**
   When the `$ref` list includes `'product'`, each directly referenced product item **must** be fetched with all other requested domains attached (e.g. `'attribute'`, `'text'`, `'media'`, `'price'`), but **must not** contain further product references of its own, regardless of whether the caller passed `'product'` in the `$ref` array.
   *Safety constraint*: The product manager must strip `'product'` from the sub‑domain list before loading the referenced items, and must not re‑add it. This prevents infinite recursion even if the product hierarchy contains cycles.

2. **Other domains – full nesting**
   For domains other than `'product'` (for example `'attribute'`, `'catalog'`, `'text'`), the system should now forward the caller’s entire `$ref` list when loading the referenced items. This allows those items to carry their own nested references naturally, without a generic depth limit.

3. **Backward compatibility**
   For product items loaded without `'product'` in the `$ref` list, the behaviour must be unchanged. For all other managers (catalog, attribute, etc.) the new forwarding of `$ref` domains should work identically to the product manager except that they do not need the product‑specific filter – they retain the full list, which does not create self‑reference loops in their data models.

**Technical notes (context only)**
- The common list‑reference loading logic in `MShop_Common_Manager_ListRef_Traits` currently calls `getRefItems()` without passing the `$domains` array. The fix should thread the domains through so that `searchItems()` uses them.
- The product manager already implements a similar pattern for creating item bases; the depth‑limit logic should be added as an override of the ref‑item loading method.
- No changes to the public API of the product manager are required – only the internal item hydration.

Fetch full referenced products limited to one level


**Title**: Product item: return `\Aimeos\Map` from catalog/supplier accessors and expose associated stock items

**Background**
The product item currently has `getCatalogItems()` and `getSupplierItems()` that return plain `array`. Elsewhere in the framework, domain-accessor methods on items consistently return `\Aimeos\Map` instances, which give callers a richer API (filtering, mapping, etc.) without breaking array access. The product item's accessors are outliers.

Separately, product items have no way to retrieve the stock items associated with a product. The stock domain is managed independently, and product managers don't populate it when products are loaded – even though `catalog` and `supplier` references are already supported via the `$ref` array in `findItem` / `getItem` / `searchItems`.

**Ask**

1. **Return `\Aimeos\Map` from `getCatalogItems()` and `getSupplierItems()`** on both the product-item interface and its standard implementation.
   - The map keys must be the domain item IDs, values the domain items, matching current behavior.

2. **Add a `getStockItems(): \Aimeos\Map` method** to the product-item interface and standard implementation.
   - Returns an associative map of stock-item IDs to stock items implementing the stock-item interface.

3. **Make the product manager load stock items when the `$ref` array requests them.**
   - When `$ref` contains `'stock'` (either as a value or as a key), stock items must be fetched and attached to the returned product items.
   - The stock type can optionally be filtered: if `$ref['stock']` is a non-numeric string, only stock items of that type should be included.
   - Stock items are looked up by product code, not product ID.
   - The stock manager exists as a standard `\Aimeos\MShop` service and supports searching by product code and type.

**Edge cases / safety**

- If no stock items exist for a product, `getStockItems()` must return an empty `\Aimeos\Map`.
- The `getCatalogItems()` and `getSupplierItems()` signature changes are **breaking** (`array` → `\Aimeos\Map` return type). Callers expecting `array` will get a `Map` object that is iterable and array-accessible via `ArrayAccess`, so existing iteration and `count()` usage should continue to work.
- The new stock loading path must not interfere with existing catalog/supplier reference loading.

Added getStockItems() to product item and returns Maps by similar methods too


When the "Autofill" order plugin copies address and service data from an existing order onto a new order, the copied items must not keep their old database identifiers. If they do, the system treats them as belonging to the original order instead of the new one, and subsequent saving can link them to the wrong base order or cause collisions.

Additionally, the plugin should only fill address/service data when the target order does not already have items of that kind, and the check that guards this for services must correctly detect an empty service list — if the check misfires (for example due to the collection type), services that are already present may be silently replaced.

Fixed moving address/service items to new order


The `\Aimeos\MW\Str` utility class contains several helper methods whose behavior deviates from reasonable expectations, especially around empty-needle inputs, multi-byte strings, and return-type consistency. We need to fix all known issues to make the helpers robust and predictable across the codebase.

### Required behavioral corrections

**`after` and `before`**
- Must return `null` when the needle is an empty string.
- `after` must return the substring *following* the full needle, not after only its first character. Currently, for a multi-character needle (including multi-byte needles), the offset is wrong.

**`starts` and `ends`**
- Must return a strict boolean (`true`/`false`), not the integer result of `strncmp`/`substr_compare`.
- Must return `false` when the needle is an empty string.

**`in` and `some`**
- Must not match an empty needle. Passing an empty string as or within the needle(s) should cause `false`.

**`slug`**
- Should produce readable slugs for strings containing multi-byte characters (e.g. CJK, accented Latin). Non-ASCII characters should be preserved as-is; only punctuation/special characters should be replaced with the separator.
- The current implementation encodes the entire string first, which damages readability for non-ASCII text. Instead, replace only specific punctuation/special characters directly on the original string.

**`strip`**
- `strip_tags` expects a string of allowed tags, but the `strip` method accepts an array. It needs to convert the array to the format `strip_tags` expects so that allowed-tag filtering actually works.

### Non-goals
- Do not change the public signatures or add new public methods.
- No BC breaks for callers that pass valid inputs.

### Edge cases to enforce
- All needle-accepting methods should treat an empty string needle as “no match” / “not found”.
- `slug` must not turn multi-byte characters into percent-encoded sequences. The separator character can appear multiple times in a row if several special characters are adjacent, and the final result should be trimmed of leading/trailing separators.
- `match` may already work correctly, but double-check that it returns a strict boolean and does not leak errors.

Added tests to Str class and improved implementation


Simplify the `\Aimeos\MShop\Customer\Item` constructors to reduce the number of parameters. Currently, the base and standard item constructors accept a flat list of arguments for address, values, list items, referenced items, and property items. Replace this with a signature that takes only the payment address, a configurable property prefix, and a values array. Child items—list items, address items, property items—should be passed inside the values array using the keys `.listitems`, `.addritems`, and `.propitems`.

Update the customer manager’s `createItemBase` method to build these values keys and to pass the password object from the context (`$this->context()->password()`) directly to the `Standard` item constructor, instead of assembling a separate password helper and salt. The `Standard` item’s `setPassword` method must use the password object’s `hash()` method, not the old `encode` method. Deprecate `getPasswordHelper` in the manager; the helper and salt fields can be removed.

The manager’s `getUser` method should simply return `$this->context()->user()` without performing its own search, and it should be marked as deprecated with a note to use `$this->context()->user()` instead.

No other behavior should change; all existing tests must pass.

Simplified customer item constructor and used password object from context


The `store()` method on the order base manager currently accepts an `int $parts` bitmap, allowing callers to choose which sub‑items (products, coupons, addresses, services) to persist. Simplify this method so it **always persists all sub‑items** present in the basket:

* Remove the `$parts` parameter from `store()` in the interface (`\Aimeos\MShop\Order\Manager\Base\Iface`) and in the standard implementation (`Standard`).
* Unconditionally call the existing per‑part store helpers (products, coupons, addresses, services) for the basket, keeping their relative order unchanged (products → coupons → addresses → services).
* The `load()` method’s `$parts` parameter must **not** change. If a basket is loaded with only selected parts (e.g. `PARTS_ADDRESS`), calling `store()` should persist only those loaded parts because the other sub‑item collections are empty.
* Update the corresponding unit‑test methods so they no longer pass a parts argument to `store()`, and adjust their assertions to reflect that only the parts that *were* loaded get stored (other parts remain empty).

Removed parts bitmap from store() method in order base manager


The `Aimeos\MW\Str` class currently implements its own ad‑hoc static macro system via a `method()` registration helper and a `__callStatic` dispatcher. We would like to retire this internal machinery and instead use the `\Aimeos\Macro\Macroable` trait, which provides the same capability across the framework through a `macro()` API and its own `__callStatic` implementation.

**What needs to change:**

1. Remove the `method()` method and the static `__callStatic` method from the `Str` class. The `Macroable` trait supplies these contracts.
2. Make `Str` use the `Macroable` trait (`\Aimeos\Macro\Macroable`). The trait will handle registration, discovery, and dynamic dispatch of macros.
3. Any internal references that call `static::method('...')` to look up a macro must be updated to use `static::macro('...')` instead, so that the trait's storage is consulted rather than the old `$methods` array.
4. The old private static `$methods` array may remain or be removed as long as macro state is correctly held by the trait.

**Acceptance criteria (behavioral):**

- `\Aimeos\MW\Str` no longer exposes a public static `method()` function.
- Code that previously registered a custom `slug` macro via `Str::method('slug', ...)` must now use `Str::macro('slug', ...)` and still override the built‑in `slug()` behavior.
- Direct static calls on macros (e.g., `Str::upper('...')`) must continue to work when a macro with that name has been registered through `Str::macro()`.
- The built‑in `slug()` method remains functional with its default implementation and correctly picks up a macro when one is set.


Add unique composite key for attributes.

We need each attribute item to have a unique key composed of its domain, type, and code, separated by pipe characters (e.g., "product|color|black"). This key should be stored in the database, indexed for fast lookups, and be searchable via the attribute manager's criteria interface. The attribute item interface should expose the key via a getKey() method. The item's toArray() method should include the key when the private flag is true. The key value must be truncated to 255 characters when stored.


**Bug Report: Duplicate Property Items on Repeated addPropertyItem() Calls**

When a property item is added via `addPropertyItem()` (from the `\Aimeos\MShop\Common\Item\PropertyRef\Traits` trait) without a database ID, a call with the same `type`, `languageId`, and `value` as a previously added item creates a duplicate entry instead of overwriting the existing one. This can result in multiple copies of the same logical property, breaking the expectation that each combination of type, language, and value is unique for a given entity.

**Expected Behavior**
- Adding a property item that has no ID (i.e., a not-yet-persisted item) should **replace** any existing property item that has exactly the same `type`, `languageId`, and `value`. Items that already have a database ID should continue to be keyed by that ID.
- The `deletePropertyItem()` method should accept items by the same composite identity (type, language, value) and not rely solely on object reference equality.
- The `getProperties()` method should return an associative array whose keys are the composite identity (for new items) or the numeric ID (for persisted items), making it trivial to deduplicate or look up properties by content.

**Current Problem**
- `addPropertyItem()` uses auto-incremented artificial keys (e.g., `'id-0'`, `'id-1'`) for items without an ID, so two logically identical items become separate entries.
- `deletePropertyItem()`, `getProperties()`, and the internal tracking of removed items do not align with this content-based identity, leading to inconsistent behavior when multiple items share the same logical key.

**Impact**
Modules that rely on property items (e.g., product properties, configuration, and other e‑commerce attributes) may accumulate duplicates, causing unexpected results in retrieval, deduplication, and deletion. The public API defined by `\Aimeos\MShop\Common\Item\PropertyRef\Iface` should remain unchanged; only the internal handling of new items needs to be adjusted.

Allow overwriting existing property items


## Ticket: `Base::get()` returns default instead of stored `null` values

**Affected component:** `MShop_Common_Item_Base` (shared e-commerce item foundation)
**Reported symptom:** Item properties explicitly set to `null` are lost on read-back — the getter falls back to its `$default` instead of returning the stored `null`.

### Steps to reproduce
1. Create any item that extends `MShop_Common_Item_Base`.
2. Call `$item->set('some_key', null)` or use a typed setter that internally stores `null` for a nullable column (e.g., `Coupon_Item_Code::setCount(null)`).
3. Call `$item->get('some_key')` or the corresponding typed getter.
4. **Actual:** The method returns the `$default` (typically `null`) even though `null` was explicitly stored. The item incorrectly reports the data as absent.
5. **Expected:** The method returns the stored `null` value. Storing `null` should be distinguishable from “key not present.”

### Root cause (internal note)
The internal `get()` helper currently uses `isset()` to test whether a key exists in the `$this->bdata` array. `isset()` returns `false` for array entries whose value is `null`, so getters that store legitimate `null`s cannot retrieve them.

### Required behavior
- `get($name, $default)` must return the actual stored value whenever the key **exists** in `$this->bdata`, regardless of whether that value is `null`, `0`, or an empty string.
- If the key does **not** exist, it should continue to return `$default` as before.
- No changes to the `set()` method or the rest of the data-storage contract are needed; this fix is strictly about the existence check inside `get()`.

### Why this matters beyond one class
Virtually every concrete item class relies on `Base::get()` to read its fields. Several fields in the database schema are nullable (e.g., `coupon.code.count` is `integer NOT NULL DEFAULT 0` but the item layer deliberately allows `null` for unset counters). If `null` cannot round-trip through the item, consumers that check for “not yet counted” vs. “counted as zero” break, and tests that validate `null` handling will fail.

### Acceptance criteria
- An item with a stored `null` value for a key returns that `null` from `get()`.
- An item with a stored `0` (or `''`) for a key returns that value, not the default.
- A key that has never been set still returns the provided `$default`.
- Existing typed getters (e.g., `Coupon_Item_Code::getCount()`) continue to work correctly for both `null` and `0` cases without changes to their own implementations — the fix should be confined to the base item layer.

Prevent wrong returns for NULL and 0 in items


Allow any salutation string up to 8 characters, replacing the hardcoded allowlist.
Rename the female-salutation constant from `SALUTATION_MRS` to `SALUTATION_MS` (keeping the existing value `'mrs'`).
Remove the constant `SALUTATION_MISS` completely.
Update all internal references to those constants to use the new name.
The validation routine must reject salutations longer than 8 characters instead of only permitting a few predefined values.

Allow custom salutations, changed "mrs" to "ms" and removed "miss"


```
The Taxrates plugin currently applies the tax rate to both order products and order services (delivery, payment, etc.). Add a backend configuration option that lets administrators restrict tax‑rate updates to products only.

- Add a new boolean configuration entry named `services`, with label “Apply to services as well”, defaulting to `true` (so existing behavior is preserved).
- When this option is set to `false`, the plugin must update only the tax rate on order products and skip all services.
```
```

Added option in Taxrates plugin to apply to products only


**Task:** Update tree node magic property access to not throw and to track modification more precisely

**Background**
The standard implementation of `MW\Tree\Node\Iface` handles property access via magic methods `__get`, `__set`, and `__unset`. Right now, `__get` throws an exception when a property does not exist, which is inconvenient when code wants to check for optional node attributes. Additionally, both `__set` and `__unset` always set the node’s `modified` flag, even when the assigned value is identical to the current one or when unsetting a key that is not present.

**Desired behavior**
1. `__get` should return `null` for a non‑existent property instead of throwing an exception.
2. `__set` should only mark the node as modified if the new value differs from the old value (or if the key did not previously exist).
3. `__unset` should only mark the node as modified if the key actually existed before removal.

**Acceptance criteria**
- Accessing a tree node property that was never set returns `null` without throwing.
- Setting a property to the same value it already holds does not flag the node as modified.
- Unsetting a property that is not present does not flag the node as modified.
- The `@throws` annotation in the interface’s `__get` documentation is removed, and the return descriptions for all three magic methods reflect the new semantics.
- Existing tests remain passing and are adjusted where needed to cover the updated behavior.

Don't throw exception if node property retrieved with __get() doesn't exist


**Title:** Add `readf`/`writef` to the filesystem abstraction and harden the Standard driver’s path handling

**Background**
The filesystem interface (`Iface`) currently has `read`/`write` for whole‑content operations and `reads`/`writes` for streaming, but there is no built‑in mechanism for efficient whole‑file copies between the remote filesystem and local storage. Users who need a local copy of a remote file (e.g. for passing to external libraries that require a file path) must combine `read` + `file_put_contents` manually, which is inefficient for larger files. Similarly, no convenience method exists to send a local file’s content into the remote filesystem without manually opening streams.

**Requested changes**

1. **New interface methods**
   Add two new methods to `Aimeos\MW\Filesystem\Iface`:
   - `public function readf($path)` – Copies the remote file at `$path` to a brand‑new local temporary file and returns the *absolute local path* of that temporary file.
     - The caller is responsible for removing the temporary file when done.
     - Must throw `\Aimeos\MW\Filesystem\Exception` if the remote file cannot be read or a temporary file cannot be created.
   - `public function writef($path, $file)` – Copies the content of the local file `$file` into the remote filesystem at `$path`, overwriting any existing remote file.
     - Should leverage stream operations internally to avoid loading the whole file into memory.
     - Must throw `\Aimeos\MW\Filesystem\Exception` if the local file cannot be opened or the remote file cannot be written.

2. **Standard driver implementation (`Aimeos\MW\Filesystem\Standard`)**
   Implement the new methods and improve robustness across the driver:

   a. **Temporary directory**
      - Introduce a new configuration key `tempdir` for the standard driver. If omitted, fall back to the system temporary directory (`sys_get_temp_dir()`).
      - On construction, verify that the `tempdir` exists (or try to create it recursively with appropriate permissions). Throw an exception if it cannot be created.

   b. **Base directory validation**
      - The `basedir` config key must now be mandatory. Throw an exception if it is missing.
      - Verify `basedir` exists (or create it) just like `tempdir`.

   c. **Path traversal protection**
      - Reject any relative path that attempts to escape the configured base directory, e.g. paths containing `..`. Reading `/../` from anywhere in the path should be blocked with an exception.

   d. **Directory auto‑creation**
      - When `write()`, `writef()`, `writes()`, `move()`, or `copy()` target a path whose parent directory does not exist, the driver must automatically create the missing directories (recursively) before performing the operation.
      - Existing directory‑related methods (`mkdir`, `rmdir`) should continue to work.

   e. **Consistent path resolution**
      - Apply the same path sanitisation (traversal check, directory separator normalisation for the current OS) to *every* method that receives a path: `isdir`, `mkdir`, `rmdir`, `scan`, `size`, `time`, `rm`, `has`, `read`, `reads`, `write`, `writes`, `move`, `copy`, and the new `readf`/`writef`.
      - The `basedir` and `tempdir` properties should be stored as resolved, normalized absolute paths with a trailing directory separator.

   f. **Stream handling refinement**
      - The `writes()` method should be refactored to use `stream_copy_to_stream()` instead of a manual read‑write loop for efficiency and correctness.

**Acceptance criteria (behavioural)**
- `readf` creates a unique temporary file containing the remote file’s data and returns its absolute path.
- `readf` throws a domain exception when the remote file does not exist.
- `writef` writes the local file’s content unchanged into the remote path.
- `writef` throws a domain exception when the local file path is invalid/unreadable.
- `write`, `writes`, `move`, `copy` automatically create parent directories that are missing.
- Paths containing `..` are rejected in all methods that accept a path argument.
- The driver works correctly on both Unix and Windows (directory separator handling).
- Existing behaviour not explicitly changed is preserved.

Improved standard file system layer and added readf()/writef() methods


```
Title: Make unit test data setup tasks idempotent and robust against repeated runs

Several unit test data setup tasks fail when executed more than once or when
run against a database that already contains some of the test data. This
blocks reliable local development and CI setups where the same database is
reused across test runs without a full wipe.

MShopAddTypeData task
---------------------
The `processFile` method wraps the entire type-insertion loop in a
transaction. If any type record already exists, the exception is caught but
the database connection may still consider the transaction as failed,
causing all subsequent inserts in the same loop to silently abort. As a
result, only a fraction of the test types appear in the database.

Expected behaviour: each type save is independent, and an individual
duplicate is silently ignored while the remaining types are still inserted.

CatalogListAddTestData and MediaListAddTestData tasks
-----------------------------------------------------
Both tasks start a database transaction before inserting list-type records
and then continue inserting list item records inside the same transaction.
When a list-type already exists (e.g. from a prior run), the transaction
breaks and the list items are never created.

Expected behaviour: list-type creation should happen outside the
transaction; only the list-item inserts need to be atomic. That way a
pre-existing type does not prevent the items from being written.

CustomerAddTestData task
------------------------
1. The task does not remove upstream test customers before inserting new
   ones. If the task is re-run, duplicate-key violations occur.
   → Before inserting, the task should delete any customers whose code
     starts with “UTC00” (the prefix used by the test data).

2. Customer-group insertion can also fail on duplicate records. The
   `saveItem` call should be wrapped so that duplicate exceptions are
   caught and ignored, matching the pattern already used for types.

3. In `addCustomerAddressData`, an error message incorrectly references a
   “refid” key, but the array key being checked is “parentid”. The message
   must reflect the actual key to make failures diagnosable.

LocaleAddTestData task
----------------------
The `cleanupSites` helper attempts to remove both “unittest” and “unit”
sites. The “unit” site may not exist (or may be intentionally left alone),
which triggers an error. Only the “unittest” site should be removed.

Acceptance criteria
-------------------
• Running the affected setup tasks multiple times (or with partially
  populated data) succeeds without any errors or missing records.
• A developer can execute the unit test setup against an existing database
  without dropping and re-creating it.
```


We need to eliminate the duplicated configuration validation logic that exists across the coupon, plugin, and service provider base classes. Currently each of these classes contains its own `checkConfig` implementation that performs type and required‑field checks on provider configuration. This duplication leads to inconsistencies and makes any future change to the validation rules more expensive than necessary.

### Requirements
- A single, centralized configuration‑validation helper must be introduced and used by the coupon, plugin, and service provider base classes.
- The helper must accept a set of criteria objects (created from the criteria array via the existing `getConfigItems` method) and a map of configuration values to check.
- It must return an array keyed by the configuration key. Each entry shall be `null` if the value is valid, or an error message string if the value is invalid.
- The validation rules must be identical to the current implementations:
  - **required**: If a criteria entry is marked as required and the corresponding configuration key is missing or an empty string, return an appropriate error message.
  - **type checks**: Validate the value against the criteria type:
    - `boolean` – only accept the strings `'0'` and `'1'`
    - `string` / `text` – must be a string
    - `integer` – must be an integer or a numeric string containing only digits
    - `number` – must be a numeric value
    - `date` – must be a string matching the pattern `YYYY-MM-DD`
    - `datetime` – must be a string matching `YYYY-MM-DD HH:mm[:ss]`
    - `time` – must be a string matching `HH:mm[:ss]`
    - `list` / `select` – the value must exist as a key (if the default is an associative array) or as a value in the `default` array of the criteria
    - `map` – must be an array
  - **invalid type**: If the criteria type is unrecognised, throw an exception (the concrete exception class should be the generic `\Aimeos\MShop\Exception`).
- The existing `checkConfig` methods in the coupon, plugin, and service provider base classes must be replaced with calls to the new helper, delegating all validation logic to it.
- All existing tests that cover the provider base classes must continue to pass.
- The new helper must have its own dedicated unit tests covering successful and unsuccessful checks for every supported type, the required‑field behaviour, and the invalid‑type exception case.


When fetching a resource with domain parameter entries that use a prefix (for example `product/catalog` or `supplier/stock`), the system does not interpret the prefixed form to limit the list-referenced records that are retrieved.
This means code that constructs domain lists using a canonical `{manager}/{domain}` scheme (e.g., `product/catalog`, `product/supplier`) cannot rely on the prefixed notation to retrieve the expected referenced entities. Instead, the entries are either ignored or produce no results, while the plain (unprefixed) domain name works correctly.

**Expected behaviour**
When a domain list entry is a string of the form `{managerPrefix}/{domain}`, the list-ref manager should strip the `{managerPrefix}/` part if it matches the prefix of the manager that owns the list items, and then use the remaining `{domain}` part as the filter for the list domain.
For entries where the prefix does not match the owning manager's prefix, the whole string should be used as the domain filter literally. This allows callers to pass domain values that intentionally refer to foreign list-item domains (or to intentionally exclude related records) while still keeping a single, uniform naming convention.

Concretely, if the product manager's `find()` method is called with domains `['product/catalog']`, it should behave identically to passing `['catalog']`: the manager should fetch catalog reference items that are linked via product list items with domain `catalog`.
Similarly, `['supplier/stock']` passed to the product manager should **not** be silently treated as `stock`; it should be used as domain `supplier/stock`, which likely matches no list items, resulting in no stock items fetched.
The stripping must be exact (the prefix plus the slash must match the complete prefix of the manager's list table) so that there is no accidental interference with domain names that happen to begin with the same characters.

**Use case / impact**
A service or integration layer often builds the domain parameter dynamically and uses prefixed forms to avoid ambiguity between different managers that might list the same domain. Without this support, such code must fall back to manual prefix removal, which is error-prone and breaks the abstraction of the higher-level API. This feature restores the capability to pass prefixed domain strings through any manager's `find()` / `search()` without additional client-side logic.

Allow domain prefixed references to limit fetched records


```markdown
**Title:** URL helper produces malformed URLs when query parameters contain special characters

**Description:**
The `transform()` method of the standard URL view helper does not sanitize query parameter values. If a parameter value – for example a product or category name used in filtering – contains characters that are reserved in URLs (spaces, `&`, `%`, `?`, `#`, `=`, `{`, `}`, `|`, `\`, `~`, `[`, `]`, `` ` ``, `^`, `_`, `/`), the resulting query string becomes malformed. This can lead to broken links, incorrect page loads, or routing failures.

The existing treatment of trailing path segments also replaces only a partial list of those characters with hyphens, which is insufficient and can create ambiguous segment boundaries (since hyphens are already used as path separators in the trailing representation).

**Expected behaviour:**
- All special/reserved URL characters in query parameter **values** should be replaced with a safe character (underscore is recommended) before the URL is built.
- The same replacement should be applied to trailing path segments, superseding the older hyphen-based cleanup.
- The sanitization must handle nested arrays of parameters.
- At a minimum, the well-known parameter names `f_name` (for product/filter names) and `d_name` (for category/dimension names) should be sanitized automatically, as they commonly carry user‑facing text.
- It should be possible to enable sanitization for **all** parameter values via a flag.
- The sanitization logic should be reusable by other URL helper implementations (e.g., for different front‑ends).

**Impact:**
Without this fix, any feature that passes customer‑facing text into URL parameters risks creating invalid URLs and a broken shopping experience.
```

Added URL helper base class and method for sanitizing parameters


**Bug: `index.text:name()` returns no results for mixed-case queries – case-insensitive name search broken, and no way to search by product URL**

---

### Summary
When using the product text search function `index.text:name()` to filter products by name, queries using mixed or upper-case letters fail to match products that are known to exist. The same name works when entered in all lower-case. Name searches should be case-insensitive, consistent with other text-based product filter functions.

Additionally, there is no built‑in search function to filter products by their URL segment (the product’s slug), which would be useful for finding products via their friendly URL path.

---

### Steps to reproduce
1. Index a product whose name contains mixed-case characters (e.g., “Café Noir Cappuccino”) and whose URL segment is “Cafe_Noire_Cappuccino” (using language “de”).
2. Create a search with the condition `index.text:name("de") ~= "Café"` (or `==`) and execute it against the product index.
3. Observe that no results are returned.
4. Repeat the search with all-lowercase `"café"` — results are returned.

### Expected behavior
- `index.text:name()` should treat product names case‑insensitively, matching regardless of how the search term is capitalised.
- There should be an `index.text:url()` search function accepting a language parameter and matching the product’s URL segment exactly (case‑sensitively for URLs, following the usual slug storage).

### Actual behavior
- Name search is effectively case‑sensitive because the stored name value is normalised to lower‑case during indexing, making mixed‑case search terms miss.
- No function exists to filter by product URL.

### Impact
- Developers cannot rely on name filters when user input may contain upper‑case characters (e.g., from auto‑complete or copy‑paste).
- No programmatic way to select products by URL segment without custom SQL or workarounds.

### Suggested fix
- Adjust the text index storage for product names so that the original case is preserved and the database’s native case‑insensitive comparison (via the existing `~=` operator) works correctly.
- Add a new non‑public string search attribute for `index.text:url()` that filters by `mindte."url"` for a given language, analogous to the existing name and relevance functions.

Added index.text:url() search function and made index.text:name() not case sensitive


**Title:** Expose the product’s site item via the product item interface

**Background**
`MShop_Product_Item` currently exposes the raw `siteid` string (via `getSiteId()` from the base item) but offers no way to obtain the full `MShop_Locale_Item_Site` object. This forces consumer code to manually look up the site from the site manager whenever it needs site-level configuration or metadata for a product.

**Request**

1. **Add `getSiteItem()` to the product item contract (`MShop_Product_Item_Iface`)**
   - Signature: `public function getSiteItem() : ?\MShop_Locale_Item_Site_Iface`
   - Returns the site item associated with the product, or `null` if none has been set.

2. **Support `’locale/site’` as a reference domain in the product manager’s search pipeline**
   - When `'locale/site'` is included in the `$ref` array passed to `MShop_Product_Manager_Standard::search()` (and `find()`, `get()`), the manager should eagerly fetch the relevant site items for all returned products.
   - The fetched site item must be attached to the product item so that `getSiteItem()` returns it.
   - The loading must follow the existing batch-loading pattern used for other reference domains (e.g., `'supplier'`, `'catalog'`, `'stock'`) – a single query that fetches site items for all product-level `siteid` values present in the result set, and then assigns each site item to its product.

**Expected behaviour**
```php
// Without 'locale/site' ref, getSiteItem() should return null (or whatever is already stored).
$product = $manager->get($id, []);
$product->getSiteItem(); // null

// With 'locale/site' ref, each product’s site item is populated.
$product = $manager->get($id, ['locale/site']);
$product->getSiteItem(); // instance of MShop_Locale_Item_Site_Iface
```

**Implementation notes**
- The product item implementation (`MShop_Product_Item_Standard`) should store the site item internally using a key that does not collide with existing reference keys (e.g., `.locale/site`), consistent with how other referenced items are stored.
- The manager should delegate the bulk fetching of site items to a new protected method similar to the existing `getStockItems()` / `getSupplierItems()` helpers, and plug it into the `search()` method at the appropriate point.
- The `search()` method’s reference handling already distinguishes between list-based domains (`['catalog', 'supplier']`) and non-list domains like `'stock'`. `'locale/site'` is a non-list domain that attaches a single site item per product; treat it accordingly.

No changes to the database schema or criteria filters are required – this is purely a convenience accessor backed by the existing `product.siteid` column and the existing `MShop_Locale_Manager_Site`.

Added getSiteItem() to product item interface


We need a Not decorator that can invert the availability of coupon and service providers. It should implement the same decorator interfaces and work exactly like the existing availability decorators (e.g., Category, Country), but negate the result: if the wrapped provider says a coupon/service is available, Not makes it unavailable, and vice versa. This allows shop owners to express conditions like “disable if product X is in the basket” by negating a decorator that checks for X. Provide the implementation for both coupon providers and service (delivery/payment) providers, along with minimal tests verifying the inversion.

Implemented negation decorator for coupon and service providers


The coupon-rebate logic in `MShop/Coupon/Provider/Base` currently creates rebate order products whose entire discount is represented as a negative `value` on the price, with a matching positive `rebate` field. This is incorrect when a discount also covers shipping costs: the shipping discount should be reflected as a negative `costs` field, not lumped into `value`. The rebate product’s `rebate` field must equal the absolute sum of the negative `value` and negative `costs`, so that the total reduction matches the applied discount.

Additionally, the tax rate used for the rebate product is derived by accumulating the basket’s prices grouped by tax rate. Existing rebate products inserted by earlier coupons are marked with the `FLAG_IMMUTABLE` flag and should not contribute to these tax-rate bucket totals. Currently they are included, which distorts the per-tax-rate totals.

Implement the following:
- When constructing a rebate product, split the discount between `-value` (price portion) and `-costs` (shipping portion). The sum of `|value| + |costs|` must equal the given rebate amount. Use the existing product’s `getValue() + getRebate()` as the price portion, and `getCosts()` as the shipping portion. Handle cases where the discount exceeds the product price: the excess should go to costs, and where it exceeds both, adjust accordingly.
- Skip any basket product whose `FLAG_IMMUTABLE` flag is set when accumulating the price-by-tax-rate map used to determine the rebate product’s tax rate.

The test fixture data for `order/base/product` must not mark normal order products as immutable. Set the `flags` of the `CNE/636.00` and `CNC/636.00` entries to `0` so they are included in tax-rate aggregation as non-immutable products.

Use negative costs in rebate products instead of total in value


**Title:** List items from `getListItem` missing referenced items and `getListItems` filtering broken

**Description:**
- When fetching a single list item via `getListItem($domain, $listtype, $refId, $active)`, the returned `Lists\Iface` object’s `getRefItem()` returns `null` even though the referenced domain item (e.g. a text, attribute, media item) is already loaded and stored inside the product/object.
  Expected: if the ref item is present in the internal ref-item storage, the list item should have it attached, so `$listItem->getRefItem()` returns the correct domain item.

- The `getListItems($domain, $listtype, $type, $active)` method does not filter correctly when both `$listtype` and `$type` are supplied. In particular:
  - Supplying a `$listtype` (list item type filter) seems to exclude **all** items regardless of their actual list type.
  - The combination of list-type and ref-item-type filters (via `$type`) does not produce the expected intersection of items whose list type and referenced item’s type both match.
  Expected:
  - When `$listtype` is given, only list items whose type belongs to the allowed list types are returned.
  - When `$type` is given, only list items whose **referenced item** has a type belonging to the allowed types are returned.
  - Both filters can be used together.

- The docblock of `getPropertyItems()` in `PropertyRef\Iface` says `$type` is `string|null`. The method signature and other similar methods (e.g. `getListItems`) allow an array of type strings as well. Update the PHPDoc to reflect that `$type` can be a string, an array of strings, or null.

**Notes:**
The fixes must not break existing access patterns for list items and reference items. The internal handling of reference items should remain lazy; attaching the ref item in `getListItem` should happen only when the ref item is already loaded.


**Coupon `isAvailable()` should check code existence and validity by default**

Right now `\Aimeos\MShop\Coupon\Provider\Base::isAvailable()` unconditionally returns `true`.
This means every coupon code that reaches a provider is silently accepted, even if the code does not exist in the database, has expired, or has been disabled.

We need to change the default behaviour so that `isAvailable()` performs a real availability check:

* It must confirm that the coupon code actually exists as a valid coupon item.
* It must take into account the standard availability filters that the coupon and coupon/code managers already provide in their default search objects (date range, status, site conditions, etc.).
* Only when a matching item is found should the method return `true`.

To make this work correctly, the default search constructed by
`\Aimeos\MShop\Coupon\Manager\Code\Standard::createSearch(true)` needs fixing.
Currently it creates a bare `\Aimeos\MW\Criteria\SQL` object directly, skipping the common base manager's search setup entirely, which means site and other fundamental filters are missing.
It should instead start from `createSearchBase('coupon')` and merge the code-specific date constraints, and the non-default branch should delegate to `parent::createSearch()`.

The `\Aimeos\MShop\Coupon\Provider\Example` provider currently overrides `isAvailable()` to return `true`. That override should be removed so the example inherits the new base availability logic. This will allow the example to demonstrate both available and unavailable code scenarios by passing different coupon codes.

Checks coupon availability in isAvailable() by default


```
Title: MySQL 5.6 setup breaks on warehouse NOT NULL migration

The `ProductChangeStockWarehouseIdNotNull` migration (which alters
`mshop_product_stock.warehouseid` to NOT NULL) fails against MySQL 5.6
because the `ALTER TABLE ... MODIFY ... NOT NULL` statement conflicts with
the existing foreign key `fk_msprost_stock_warehouseid`.

Additionally, the migration can execute before the environment-specific
warehouse data has been written (default, unitperf, unittest), causing the
initial `UPDATE ... SET warehouseid = ... WHERE warehouseid IS NULL` to miss
rows that were inserted later, and the subsequent NOT NULL constraint then
fails.

Acceptance criteria:
- The migration must complete without foreign key errors on MySQL 5.6.
- The dependency ordering must guarantee that warehouse data is present
  before the migration runs, regardless of environment (default /
  unitperf / unittest).
- Behaviour on other MySQL versions must remain unaffected.
```


Title: Allow "value" view helper to extract from arbitrary arrays instead of being tied to request params

The `value` view helper (`MW\View\Helper\Value`) currently retrieves only request parameters via `$this->getView()->param()`. We need to generalize it so that it can be used with **any associative array** supplied by the caller (e.g., configuration arrays, product data, arbitrary nested structures).

**Expected behavior change**
- The `transform()` method should accept the target array as its first argument.
- The key traversal (splitting by `/` and descending into sub‑arrays) and the default-fallback behavior must remain exactly as they are today.
- The helper must not depend on or access the view's parameter helper anymore.
- The interface in `MW\View\Helper\Value\Iface` must be updated to reflect the new signature.

**Constraints / edge cases**
- The input array may be multi‑dimensional and keys are separated by `/`; leading/trailing slashes should be handled as they are now.
- If a key path does not exist, the provided default value (or `null` if no default is given) is returned.
- All existing downstream code that currently passes only `(key, default)` must be updated to supply the array – those usages are out of scope for this ticket but keep it in mind when designing the signature change.

**Acceptance criteria**
- The helper no longer fetches data from the view’s param storage.
- The public API allows `transform(array $values, $key, $default = null)` and works for nested keys like `'list/test/key'`.

Changed "value" view helper to work on arbitrary arrays


The `addProduct()`, `setAddress()`, and `setService()` methods on the order basket item (`\Aimeos\MShop\Order\Item\Base\Iface`) currently return the inserted position or the stored address/service item. However, the basket implements an observer pattern: internal listeners are notified after the item is stored, and those listeners can modify or even **remove** the item from the basket.

When a listener removes the item, the value returned by the method no longer represents the basket’s true state. For example, `setAddress($address, 'delivery')` might return the address object, but after plugin processing the delivery address could have been dropped. Relying on the return value leads to logic that acts on a stale or non-existent item.

**Expected behavior**
- `addProduct()`, `setAddress()`, and `setService()` should not return anything; they should be void.
- After calling these methods, consumers who need the final stored item must explicitly retrieve it via `getProduct()`, `getAddress()`, or `getService()`.
- The docblocks on both the interface and the abstract base implementation must be updated to remove the documented return values.
- Any internal call-sites that previously captured the return value (e.g., for positioning or identity checks) must instead fetch the data from the basket after the call, using the appropriate getter.

This is an intentional breaking change to the public API, but it is necessary because the current contract cannot be honored when plugins are active. The goal is to eliminate the false promise that the returned item is the definitive one present in the basket.

Returning updated address/service item doesn't work


**Bug: DB statement parser mis-handles escaped apostrophes, causing SQL errors on literal strings**

## Observed behavior
When I prepare and execute a database statement that contains a string literal with certain escaped apostrophe sequences (for example, a backslash followed by two single quotes: `\'\''`), the database abstraction layer throws a syntax error or misinterprets the query. This occurs even when the SQL has no `?` parameter placeholders and is executed directly via the connection's `create()` / `execute()` flow.

## Expected behavior
Valid SQL statements that include escaped apostrophes inside string literals should be accepted and executed without errors. The internal SQL splitting logic should correctly identify and skip quoted string segments so that:
- A statement with no `?` placeholders is passed through unchanged.
- Placeholder detection is not confused by quote-escaping sequences (`''` for a literal apostrophe, or backslash-escaped quotes) inside string literals.

## Impact
This prevents inserting or comparing string values that contain legitimate escaped apostrophes, breaking data import scripts and dynamic SQL generation. The same problem affects both PDO and DBAL database drivers.

## Suggested fix direction
The code that splits SQL into parts for placeholder binding needs to strip escaped quote sequences in the correct order before counting single quotes to determine whether the parser is inside a string literal. The stripping order should handle backslash-escaped quotes (`\'`) before doubled quotes (`''`), otherwise a stray backslash can cause an off-by-one quote count and break the boundary detection. An early return when no `?` is present would also avoid unnecessary processing.


Remove the behavior in the CSV writer that replaces end-of-line characters inside data fields with a substitution character. Currently, when a value passed to the `add()` method of the CSV content container contains the configured line-end sequence, that sequence is silently replaced by the `csv-lineend-subst` character (defaults to a space). This corrupts field content that legitimately includes newlines, making it impossible to produce multiline quoted fields per CSV conventions.

The desired behavior is to preserve embedded line breaks within a field as-is when writing the CSV record. The `csv-lineend-subst` configuration option and its associated property should be removed entirely, along with the substitution logic in `add()`. The record-delimiter line-end setting should remain unchanged.

Removed replacing EOL characters in texts for CSV files


## Request: Order base service items should expose underlying service item when loaded with `service` ref

### Symptom
When I fetch order base service items via `MShop\Order\Manager\Base\Service\Standard::searchItems()` and pass `'service'` in the `$ref` array, the returned order service items do **not** contain the original `MShop\Service\Item\Iface` object. There is no method on the order service item to access the underlying service definition (name, type, provider code, configuration, etc.). This forces callers to perform a separate service lookup using the `serviceid` present in the order record, which is inefficient and inconsistent with how other order managers work.

### Expected behavior
The order base service manager should support the same reference-loading pattern as the existing order base **product** manager: when the `$ref` parameter includes `'service'`, the manager should fetch the corresponding service items in a single batch and attach them to the order service results.

The order service item should then expose the attached service item through a dedicated accessor (e.g., `getServiceItem()`) so callers can retrieve:
- Service type
- Provider code
- Configuration array
- List items (prices, texts, media) if they were requested through `$ref`

If `'service'` is **not** passed in `$ref`, the accessor should return `null` (no unnecessary queries).

### Why this matters
- **Consistency** – the order product manager already does this for `'product'` references. For every order-based entity that references another domain, the same lazy/eager-loading convention should apply.
- **Performance** – callers currently must loop over order services and call `\Aimeos\MShop::create($context, 'service')->getItem($serviceId)` for each item, which generates N+1 queries. A batch ref-load avoids that.
- **Completeness** – displaying or configuring services in the checkout/order summary often requires the original service item fields.

### Constraints / edge cases
- The ref key can be either a value in a non-associative array (`['service']`) or an associative array entry (`['service' => ...]`). Both should trigger loading.
- The service manager’s `searchItems` should be called with a slice limited to the distinct, non-null `serviceid`s from the order services. Only fetch the services that actually exist.
- The `$ref` list must be passed through to the inner service manager call so that requested sub-references (like `media`, `text`, `price`) are also loaded on the service items.
- Existing search, total counting, and attribute loading must continue to work exactly as before. The only new behavior is that a non-null service item object becomes available when requested.
- The order service item’s method for retrieving the service item should respect nullable semantics: when no service item is available, it should return `null`.

### Suggested minimal public API shape
On the order service item (in `MShop\Order\Item\Base\Service\Iface`):

```
public function getServiceItem() : ?\Aimeos\MShop\Service\Item\Iface;
```

And the manager should internally resolve the reference in `searchItems()` analogously to how `MShop\Order\Manager\Base\Product\Standard::searchItems()` resolves `'product'`.

Fetch service item in order base service manager if requested