# `MishkaCmsMigration.Workers.Helpers.Decision`
[🔗](https://github.com/mishka-group/mishka_cms_migration/blob/v0.0.1/lib/mishka_cms_migration/workers/__helpers/decision.ex#L5)

What a dry run or an import does with each item of a page (`call/3`), against what earlier runs
recorded for the site (`MishkaCmsMigration.MigrationItem`), found by the run's source host, the
item's kind and its source id. An admin's choice on the item comes first, before anything the
source or the CMS says:

  * the admin left it out (`left_out`) — `{:skipped, "left_out_by_admin"}`, whatever the source
    changed and whatever the CMS holds; a CMS record an import wrote for it is kept as it is;
  * the admin mapped it to a CMS record (`mapped_id`) — `{:mapped, target, record}`, the record,
    which the import connects it to and never writes, whatever the source changed, what an
    earlier run wrote for it and what the target's `existing/2` would find; when that record is
    gone, `{:skipped, "mapped_missing"}`, and when an admin archived it, as below.

Then, what earlier runs recorded:

  * the CMS record it was written or mapped to is archived (see "An archived record") — it waits
    for the admin's decision, `{:undecided, "archived_in_cms"}`, whatever the source changed;
  * none, or one never written — `{:created, target, nil, decisions}`, unless the target finds
    it in the CMS already (`MishkaCmsMigration.Target`'s `existing/2`):
    `{:connected, target, record}`, the CMS record it is, `{:updated, target, record, decisions}`,
    the CMS record it is written over, `{:created, target, nil, decisions, prepared}`, created
    with what the target found for its write, `{:skipped, reason}`, why it is left out, or
    `{:undecided, conflict}`, a conflict on the site no admin has decided yet;
  * the CMS record it was written to is gone, deleted for good (see "A deleted record") — it
    waits for the admin's decision, `{:undecided, "deleted_in_cms"}`, whatever the source changed;
  * its `MishkaCmsMigration.Targets.Helpers.LanguageUrls.hash/2` is the one recorded —
    `{:skipped, "unchanged"}`: an item that writes links is changed, to be written again, when
    the site's language address mode changed since it was written;
  * it was connected to a record the CMS already held (no `target_updated_at` was recorded): the
    record is the CMS's own and is never written — `{:skipped, "connected"}`;
  * the CMS record's `updated_at` is not the one recorded when it was written: an admin changed
    it in the CMS — `{:skipped, "edited_in_cms"}`;
  * otherwise — `{:updated, target, record, decisions}`, the CMS record to write over.

## An archived record

A resource with `AshArchival.Resource` hides a record an admin archived from its reads, so an
item's record that is not read is looked for among the archived ones
(`MishkaCmsMigration.Workers.Helpers.Archived`), in one more query for the page and only when one
is not read. Writing over an archived record, or pointing another at it, such as a post's tag,
is refused, so the item waits for the admin's decision on `"archived_in_cms"`, in a dry run and in
an import, and what points at it waits for that decision too
(`MishkaCmsMigration.Workers.Helpers.Needs`). Leave it archived (`"leave_out"`) leaves the item
out, as `"left_out_by_admin"`, and what points at it is written without it. Restore it
(`"import"`) is `{:restored, target, record, decisions, then}`: the import restores the record as
the admin and goes on as it would have, had it never been archived (`then`):

  * `:written` — the source changed it and no admin did, so it is written over;
  * `:unchanged` — the source did not change it, so it is kept as it is, and recorded as the
    import's own again;
  * `:kept` — an admin changed it before archiving it, or it is a record the CMS already held or
    the admin mapped the item to, so it is kept as the admin left it.

## A deleted record

An item's record that is read neither among the live ones nor among the archived ones was deleted
for good, so it is known in the same reads, with no query of its own. There is nothing to write
over, and what points at it would lose it, so the item waits for the admin's decision on
`"deleted_in_cms"`, in a dry run and in an import, and what points at it waits for that decision
too, as for an archived one. Import it again (`"import"`) is `{:created, target, nil, decisions}`:
the item is planned as a first import plans it, the target's `existing/2` asked about it with the
page's other new items, and the import records the new record in place of the deleted one and
forgets the decision (`MishkaCmsMigration.Workers.Helpers.Writer`), so it holds once: a later
deletion of the new record asks again. Leave it out (`"keep"`) is `{:skipped, "target_missing"}`
on every later run, and what points at it is written without it. A record an admin mapped the
item to that is gone is `"mapped_missing"` instead, above: the import never wrote it.

`decisions` are the admin's answers to the conflicts the item met
(`MishkaCmsMigration.MigrationItem`'s `decisions`), which the target is handed with the item
(`MishkaCmsMigration.Target.decision/3`); `existing/2` is handed those of the page's items it is
asked about.

The CMS records are read through the target resource's primary read as the admin running the
job, under the CMS's own policies. When the CMS does not let the admin read them, each item that
needs its record (one an earlier run wrote, or an admin mapped) fails as
`{:failed, "forbidden: <kind>"}`, "You may not change <kinds> on this site", and the walk goes
on; any other error of the read fails the attempt, as an error of the walk does
(`MishkaCmsMigration.Workers.Helpers.Pipeline`).

An item whose kind has no target (`MishkaCmsMigration.Target`) is `{:skipped, "no_target"}`,
unless it is a kind the Blog writes (`MishkaCmsMigration.Application.blog_kinds/0`) on a node
without the Blog: then it waits for an admin's decision, `{:undecided, "blog_missing"}`, until the
admin left it out (`{:skipped, "left_out_by_admin"}`) or decided to wait for the Blog
(`{:skipped, "waits_for_blog"}`), and a later run, once the Blog is there, plans it as any other.
One the run's choices (`MishkaCmsMigration.MigrationRun`'s options) leave out wherever the Blog
is, as its target does, is `{:skipped, "not_chosen"}` instead, with nothing to decide: a comment
while `comments` is off (`MishkaCmsMigration.Targets.BlogComment`), and a post's old address
while `post_address` is `"blog_without_redirects"` (`MishkaCmsMigration.Targets.PostRedirect`).

An item no run has met that the run's choices leave out (`MishkaCmsMigration.MigrationRun.left_out?/3`:
a kind switched off in the New import window, or a site setting such as the icon) is planned as
the admin's own leave-out, `{:skipped, "left_out_by_admin"}`, and recorded as left out
(`MishkaCmsMigration.Workers.Helpers.Writer`), so the Items page lists it as Left out by you and
Bring back undoes it. An item a run has met already keeps what the admin did with it.

Items are found by source host as well as by kind and id, so several sources moved into one site
never meet: the same id from two hosts is two items, each with its own CMS record.

A page is read twice, whatever its size: once for the items earlier runs recorded, once for the
CMS records they were written to, and a third time when one of those records is not read, for
the archived ones, which tells the deleted ones too; and its items no run has written, or an
admin decided to import again, are handed to the target's `existing/2` together, once.

# `decisions`

```elixir
@type decisions() :: %{required(String.t()) =&gt; String.t()}
```

An admin's answers to the conflicts an item met, by conflict.

# `plan`

```elixir
@type plan() ::
  {:created, module(), nil, decisions()}
  | {:created, module(), nil, decisions(), term()}
  | {:updated, module(), struct(), decisions()}
  | {:restored, module(), struct(), decisions(), :written | :unchanged | :kept}
  | {:connected, module(), struct()}
  | {:mapped, module(), struct()}
  | {:skipped, String.t()}
  | {:undecided, String.t()}
```

What to do with an item: write a new record, with what the target found for its write when it
found anything, write over `record`, restore `record` an admin archived and go on as `then` says
(see "An archived record"), record it against `record` the CMS already held, leave it, and why,
or wait for an admin's decision on a conflict.

# `call`

```elixir
@spec call(map(), String.t(), [MishkaCmsMigration.Source.Item.t()]) :: %{
  required(String.t()) =&gt; plan()
}
```

The plan for each of `items`, a page of `kind`, by source id.

# `record_conflicts`

```elixir
@spec record_conflicts() :: [String.t()]
```

The conflicts on the CMS record an item was written to, `"archived_in_cms"` and
`"deleted_in_cms"`: what points at the record waits for the admin's decision on them
(`MishkaCmsMigration.Workers.Helpers.Needs`), and an item that meets one under a parent an admin
left out waits for it too (`MishkaCmsMigration.Workers.Helpers.Pipeline`).

---

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