# `PhoenixKitCatalogue.Import.Pro100Plan`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.19.0/lib/phoenix_kit_catalogue/import/pro100_plan.ex#L1)

Builds the PRO100 sync diff/report. Rows fall into three buckets:

  * `:updates` — matched rows become per-field change sets (`base_price`,
    materials `unit`) plus a `data["pro100"]` round-trip blob
  * `:creates` — rows with no catalogue match but enough data to become a
    new item; applied only when the operator opts in on the preview screen
  * `:skipped` — ambiguous matches, rows belonging to another group, and
    unmatched rows that cannot be created (no id, no name)

Pure — no DB. All three buckets are computed unconditionally so the preview
can show counts before the operator decides what to apply.

# `build`

```elixir
@spec build([map()], PhoenixKitCatalogue.Import.Matcher.t(), String.t() | nil) :: %{
  updates: [map()],
  creates: [map()],
  skipped: [map()],
  stats: map()
}
```

# `split_name`

```elixir
@spec split_name(String.t() | nil) :: {String.t() | nil, String.t()}
```

Splits a PRO100 row name into `{category_name_or_nil, item_name}`.

Both parts are trimmed. When there is no whitespace-padded separator — or
when either side of it is blank ("/ Foo", "Group /") — the whole trimmed
string becomes the item name and there is no category. A blank category name
would fail `create_category`'s changeset, which the executor swallows
silently, leaving items uncategorized with no diagnostic.

# `to_executor_plan`

```elixir
@spec to_executor_plan([map()]) :: %{
  items: [map()],
  categories_to_create: [String.t()]
}
```

Converts the `:creates` bucket into the plan shape `Executor.execute/4` wants.

Category linkage runs through the `:_category_name` placeholder resolved
against the phase-1 lookup, which is built from `categories_to_create` — a
name present on an item but missing from that list is dropped silently, so
the two must be derived together.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
