# Forms, leads and CRM boards

**Status:** current · **Verified:** 2026-10-25, source review; no live submissions · **Owner:** backend
**Serves:** `GET /docs/forms`

## 1. What you can do here

Create a form, insert its widget or open it from a button, submit a custom quiz
through the platform intake, and manage leads in CRM boards. This is the forms
contract previously in `/docs/webcoding` §§13–14.2. The endpoint schema and field
constraints are available at `/openapi.json?scope=forms,leads`; neighbouring
contracts: `/docs/widgets-area`, `/docs/own-markup`, `/docs/tasks` and `/docs/webhooks`.

## 2. Decide first (forks)

| If you want… | Take this path | Cost |
|---|---|---|
| A platform form inline | Create `/forms`, use `kind:"form"`, `props.form.formId` | Appearance lives in the form schema; changes affect every insertion |
| A button opening a platform popup | A platform button target `{type:"form",id}` or `<a href="form:7">Open form</a>` | Platform owns the popup and submission |
| A custom level-2 quiz/calculator | `vizen-form-open` or `vizen-form-submit` bridge, §4.5 | Author supplies UI; platform schema and validation still apply |
| A standalone site folder | Public form endpoints, §4.4 | No parent runtime/popup host; `credentials:"omit"`, explicit consent |
| A level-3 interactive form | Public intake, §4.4; isolation in `/docs/own-markup` | A complete level-3 browser recipe was not verified in this review; guarded pages do not run authored JS |
| A CRM integration | `lead.created`, `/docs/webhooks` | Receiver checks test events and deduplicates deliveries |
| A test without external delivery | Owner's authenticated dev preview, §4.7 | Still stores a test lead in the operational table; not a PAT-dev loop |
| A delivery test | `/forms/{id}/test-lead`, §4.7 | Sends real email/Telegram/webhook requests |

A **CRM sandbox** is a shared lead board; a **dev contour** is a form/design
draft; an **iframe sandbox** isolates authored JS. They are different things.
Creating a webhook creates neither a board nor a test environment.

Level 1 removes `<form>`, `<input>` and `<select>`; use the widget or a form
link there. `vz-lead` is reserved and has no handler. Do not put a PAT, webhook
secret or notification credential in HTML or a public schema.

## 3. Objects and where they live

### 3.1. Permissions, ownership and transport

Read `/docs/authentication` and the key passport before a write. `forms:read`
reads forms and boards; `forms:write` edits forms and lead boards. `leads:read`
reads/exports leads and history; `leads:write` edits/deletes leads. Both `leads:*`
scopes are sensitive: neither `forms:*` nor `catalog:*` grants them. A missing
scope gives `PAT_SCOPE_MISSING`; an incompatible contour gives
`PAT_CONTOUR_MISMATCH`. Use `/docs/scopes.json` for the generated method map.
Board endpoints also accept `tasks:*` for task boards; handlers check the board
kind, so forms rights do not open task content and task rights do not open leads.

Private methods derive the shop from the authenticated caller. Supplying another
`company_id` cannot switch shops. Public intake derives it from the form.
Unavailable/foreign resources use the same not-found mask.

Forms are contour-aware content. Leads, CRM boards and transitions are live
operational data, outside draft publication. A dev PAT can read `/sandboxes`
and edit its draft forms; lead reads and live operations (including board
writes and `test-lead`) are denied. Check actual capabilities, not a preset name.

Paths below have **no `/v1` prefix**. `schema`, `values`, `utm`, `snapshot`,
board/column `settings`, and exported `csv` are proto **bytes**: JSON carries
base64 of the raw JSON, or base64 file contents for CSV. Do not send nested
objects in these wire fields. Int64 identifiers are strings in JSON responses;
counters such as `fields_count`, `new_leads`, `total` and `rows` are numbers.

### 3.2. Read/write routes and response envelopes

| Request | Permission | Response |
|---|---|---|
| `GET /forms?page.number=1&page.limit=50` | `forms:read` | `{result:[…],total,currentPage}`; array directly in `result` |
| `GET /forms/{id}` | `forms:read` | `{result:Form}` |
| `POST /forms {"item":{…}}` | `forms:write` | `{result:Form}` |
| `PUT /forms/{id} {"item":{…}}` | `forms:write` | `{result:Form}`; partial update |
| `DELETE /forms/{id} {}` | `forms:write` | `{deleted_after:…}`; soft deletion, 30-day delay |
| `GET /forms/{id}/usage` | `forms:read` | `{usages:[{entity_type,entity_id,title}]}` |
| `GET /forms/{id}/public` | public | `{result:{id,schema,is_active,banner_url}}` |
| `POST /form-leads` | public | `{ok,success_text}`; no lead id |
| `GET /form-leads` | `leads:read` | `{result:{items:[…],total,currentPage}}` |
| `GET /form-leads/{id}` | `leads:read` | `{result:FormLead}` |
| `GET /form-leads/export` | `leads:read` | `{csv:"<base64>",rows:N}` |
| `PATCH /form-leads/{id} {"status":"done"}` | `leads:write` | `{result:FormLead}` |
| `PATCH /form-leads/{id}/card` | `leads:write` | Partial card update |
| `DELETE /form-leads/{id} {}` | `leads:write` | `{}`; immediate permanent deletion |

DELETE requests routed through the gateway need at least `{}` as their body.
The static `/form-leads/export` route is matched before `/{id}`.
`Form` includes `id,name,code,schema,is_active,sandbox_id,created_at,updated_at`.
List items omit the schema and add `fields_count,new_leads`; the lead count is
from the live table in either contour. `FormLead` includes `id,form_id,form_name,
values,snapshot,status,page_url,referer,utm,ip,user_agent,is_test,created_at` and
board/card fields. `form_name` comes from the stored snapshot, surviving form
rename/deletion.

Card input keys: `title`, `amount_kop`, `assignee_user_id`; omitted keys stay
unchanged. Use `clear_amount:true` or `clear_assignee:true` to clear them. The
amount is in minor units (the `_kop` field), not the catalogue price unit.

Lead list/export filters: `filter.form_id`, `filter.status`
(`new|in_progress|done|spam`), `filter.from`/`filter.to` (RFC3339),
`filter.include_test` (default false), `filter.sandbox_id`, `filter.column_id`,
`filter.is_spam`, `filter.utm_source`, `filter.utm_medium`. UTM matching is exact,
case-insensitive, on flat keys only; source and medium combine with AND. Empty
filter = absent; missing attribution does not match a source filter. Values
are seller-defined, not a selectable API dictionary. Lists are newest first;
forms default to 50 per page, leads to 20; `page.number` starts at 1 and the
maximum page size is 100. Export is unpaged, up to 10,000 rows.

CSV is UTF-8 with BOM, semicolon-delimited and CRLF. Columns are
`created_at,stage,form,page_url`, then the alphabetical union of field keys,
then attribution keys and their `first_` counterparts. Untrusted formula-like
values are escaped as text.

### 3.3. The form schema

`forms.schema` holds fields, appearance, banner, button, consent and notification
configuration. A widget only holds the form reference. Schema updates replace
the whole schema: read-modify-write and preserve unknown keys, which the server
retains. An omitted schema creates an empty `{}` for the owner to finish.

| Node | Keys and meaning |
|---|---|
| Root | `v:1` |
| `fields[]` | Stable `id`; `kind`; unique collecting `key`; `label`, `placeholder`, `required`, `value`, `options[]`, `hidden` |
| `fields[].kind` | `text,textarea,email,phone,number,select,radio,checkbox,consent,date,file,hidden,heading,description,divider,submit` |
| `fields[].key` | `^[a-z][a-z0-9_-]*$`; collecting fields require it; decorative/service kinds do not |
| `layout` | `mode:vertical|inline`, `width` (280–1200, default 460), `gap` (0–48, default 16), `align:left|center|right` |
| `banner` | `pos:none|top|bottom`, `fileId` owned media UUID (not a baked URL), `title,text,note` |
| `look` | `bg,backdrop` are Color objects, not bare hex strings; `radius` 0–40 (default 24), `size:s|m|l`, `showClose`, `button:{label,style,width:auto|full}` |
| `texts` | `title,text,success`; `texts.title` plus a heading field renders two titles |
| `consent` | `enabled,label,link`; link is `{type:"url",url:"…"}` or `{type:"page",id:N}` |
| `submit` | `targets:[{type:"email"|"telegram",address}]`, `antispam`, `confirmToSender`, `confirmText` |

Unknown rendering enums fall back safely and numeric appearance settings are
clamped by the widget normalizer. A `hidden` field carries calculator values;
`fields[].hidden:true` instead hides the element from rendering. A `file` kind
exists in the schema but attachments are not accepted by v1 intake.

Validate before writing with `POST /docs/validate {"form_schema":{…}}` (here the
validator expects a raw object, not transport base64). Schema violations return
`unknown` findings with the reason. Duplicate visual titles return `ignored`
warning findings even though the write is allowed.

## 4. Recipes

### 4.1. Create or edit without duplicating a form

First read `/forms` and `/forms/{id}/usage`; reuse a matching purpose. Create a
second form only when its schema must differ from existing insertions. Example
schema before base64 encoding:

```json
{
  "v": 1,
  "fields": [
    {"id":"f1","kind":"text","key":"message","label":"Message","required":true},
    {"id":"f2","kind":"consent","key":"consent","required":true},
    {"id":"f3","kind":"submit"}
  ],
  "texts":{"title":"Contact us","success":"Thank you"},
  "consent":{"enabled":true,"link":{"type":"url","url":"/legal/privacy"}}
}
```

Send `POST /forms {"item":{"name":"Contact","code":"contact",
"is_active":true,"schema":"<base64 of the JSON above>"}}`. These are explanatory
placeholders; substitute the actual encoding. `is_active` defaults to true on
create, but send it explicitly. For edits, `PUT /forms/{id}` changes only supplied
keys; `"code":""` clears the machine code. Read the form back, decode `schema`,
and compare intended fields and `is_active` before inserting it. To disable
intake, update `is_active:false`; check usages before deletion.

### 4.2. Insert a form widget or an opener

Create a normal content block with this section:

```json
{"type":"text","payload":{"v":2,"kind":"form","props":{"form":{"formId":7}}}}
```

Use the returned block id in the page bindings: the attachment/replace-set
recipe is `/docs/webcoding` §6, wrapper settings are `/docs/widgets` (JSON).
The form appearance belongs in its schema, not in the block props. Missing or
zero `formId` makes the widget empty and absent from the storefront.

For a platform button choose a form target `{type:"form",id:7}`. Own level-1
HTML can use `<a href="form:7">Open form</a>`; the canonical numeric-id scheme
`form:<id>` survives sanitization. Malformed targets are removed. Do not depend on `data-*`
or inline click handlers in level 1. Open the storefront page and click the
button; verify the expected form and fields appear, not merely a saved block.

### 4.3. Read or export leads

With a live key and `leads:read`, request
`GET /form-leads?filter.form_id=7&page.number=1&page.limit=50`. Decode `values`,
`snapshot` and `utm`; do not copy customer data into logs or reports.
`PATCH /form-leads/{id} {"status":"in_progress"}` needs `leads:write`.
Read back the lead to verify its status. Export the same filter via
`GET /form-leads/export?filter.form_id=7`, decode `csv`, and compare row counts
and column names without publishing row contents. Test leads require
`filter.include_test=true`; it changes reading, not submission mode.

### 4.4. Public intake and standalone site folders

`GET /forms/{id}/public` returns the schema with `submit` removed; `code` is not
in the public response. Other unknown schema keys remain public, so do not put
secrets there. `banner_url` resolves the owned `banner.fileId`; foreign or dead
media degrades to an empty URL. Missing/deleted forms and hidden shops are
masked as `FORM_NOT_FOUND`; normal resource-access checks still apply. The
public projection needs no token. An owner's dev projection requires the
appropriate authenticated context and, for PAT, `forms:read`.

Public submission wire shape:

```json
{
  "form_id": 7,
  "values": "<base64 JSON object of schema-key to string value>",
  "page_url": "https://shop.example/contact",
  "referer": "https://source.example/",
  "utm": "<base64 JSON attribution object, or omit>",
  "hp": "",
  "elapsed_ms": 8400
}
```

Send it as JSON to `POST /form-leads`. All required fields and enabled consent
must be provided by the visitor. Never pre-check consent in code. Email has a
soft format check (`@` and a dot); phone requires at least seven digits.
Invalid real submissions get `FORM_VALIDATION_FAILED`. The response is
`{ok:true,success_text:"…"}` without a lead id. Disabled intake returns
`{ok:false}` and stores nothing; success text comes from the form, not a
server-invented phrase. Verify storage through the selected test mode in §4.7;
`ok:true` alone is not evidence of a new row.

Standalone site folders have no parent form host, cookies or browser storage.
Use these public routes with `credentials:"omit"`. For literal `Origin:null`,
the narrow CORS entry permits only GET `/forms/{positive-id}/public` and POST
`/form-leads`, with Accept/Content-Type preflight. Authorization, Cookie,
X-Vizen-Contour and X-Vizen-Draft are forbidden by presence. `Origin:null`
confers no tenant access and cannot provide a required resource-access cookie
grant. See `/docs/file-storage` for the isolation/CDN contract.

The server whitelists string `values` by collecting field keys, excludes service
and file fields, truncates long values, and filters marketing keys to
`utm_source,utm_medium,utm_campaign,utm_content,utm_term,gclid,yclid,fbclid,ysclid,ttclid`
and the same keys under `first`. URL fields only retain HTTP(S) URLs.
A nonempty `hp` or `elapsed_ms` from 1 to 1999 yields an ordinary success response
but stores spam. Measure elapsed time honestly; do not submit on page load.
Sequential identical nonempty values within one minute normally collapse to one
lead and one set of notifications. Check/insert are not atomic: concurrent
retries have no exactly-once guarantee and no idempotency-key contract.
Over the per-form daily cap, success is returned without storage or delivery.
IP is truncated at write time (IPv4 /24, IPv6 /48); consent snapshot includes
its text/link and acceptance time. Retention defaults to 365 days, followed by
permanent cleanup. Explicit lead deletion is immediate.

### 4.5. Level-2 iframe bridge

The HTML-block iframe sends to its parent with target origin `'*'` because its
origin is opaque. This bridge belongs to an embedded HTML block; a standalone
site folder does not acquire it automatically.

```js
parent.postMessage({
  type: 'vizen-form-open', id: 7,
  prefill: { calc_total: String(124500), calc_config: 'Example configuration' }
}, '*');

// After the visitor supplies required fields and consent:
parent.postMessage({
  type: 'vizen-form-submit', id: 7,
  values: { message: 'Example request', consent: 'true' }, reqId: 'request-1'
}, '*');

addEventListener('message', (event) => {
  if (event.source !== parent || event.data?.type !== 'vizen-form-result') return;
  // Match event.data.reqId; use ok, success_text and error in your UI.
});
```

`vizen-form-open` displays the platform popup and prefills schema fields;
calculator data must have declared hidden schema fields. Consent is not
prefilled. `vizen-form-submit` retains your UI and submits without a popup;
the page host supplies page URL, attribution, honeypot and elapsed time. A reply
is `{type:'vizen-form-result',reqId,ok,success_text}` or
`{type:'vizen-form-result',reqId,ok:false,error:'validation'|'network'}`.
Correct data before retrying `validation`; network failures may be retried with
the deduplication limits above. An inactive form can return `ok:false` without
an error code, with closed-intake text supplied by the host.

`id` must be a positive integer number, not a string. `values` is required for
submit. Keys follow schema-key grammar; values are strings, invalid pairs are
removed, and only declared form fields reach intake. Oversized payloads or
invalid ids drop the message, with console warnings. A bad/oversized `reqId`
is omitted from the reply rather than invalidating the submission. Bridge
limits are in §6. Sending before two seconds is spam under the public intake
rule. Verify a real click opens the popup with expected prefill, then verify
storage/delivery through the explicitly chosen test path, §4.7.

### 4.6. CRM boards, stages and history

A form's optional `sandbox_id` selects its board; absent/zero uses the default
board. New leads enter its single intake column. The flow is UI → form → board
intake → lead → webhook. Lead operations require a live contour.

| Need | Request | Permission |
|---|---|---|
| Boards with columns | `GET /sandboxes` → `{result:[{id,name,is_default,sort_order,columns:[{id,label,kind,sort_order}]}]}` | `forms:read` |
| Create/rename/delete board | `POST /sandboxes {name}`, `PUT /sandboxes/{id} {name}`, `DELETE /sandboxes/{id} {}` | `forms:write` |
| Board order | `POST /sandboxes/reorder {ids}` | `forms:write` |
| Add/edit/delete/order columns | `POST /sandboxes/{sandbox_id}/columns {label}`, `PUT /sandbox-columns/{id} {label}`, `DELETE /sandbox-columns/{id} {}`, `POST /sandboxes/{sandbox_id}/columns/reorder {ids}` | `forms:write` |
| Assign form | `PUT /forms/{id} {"item":{"sandbox_id":3}}` | `forms:write` |
| Move lead | `PATCH /form-leads/{id}/column {column_id,comment}` | `leads:write` |
| Bulk move/delete | `POST /form-leads/bulk/column {ids,column_id,comment}`, `POST /form-leads/bulk/delete {ids}` | `leads:write` |
| Spam/unspam | `POST /form-leads/{id}/spam {is_spam:true|false}` | `leads:write` |
| Manual lead | `POST /form-leads/manual {sandbox_id,name,phone,email,comment}`; intake, `form_id=0` | `leads:write` |
| History | `GET /form-leads/{id}/events` → `{result:[…]}` | `leads:read` |

Do not delete a board/column merely to test it. Intake cannot be removed;
board deletion moves its leads to the default board, column deletion to intake,
bypassing transition rules. Spam marking is reversible; deletion is not.
Re-read the form and `/sandboxes` after assignment; verify a lead's `sandbox_id`,
`column_id`, `column_label`, `is_spam` and history after an authorized move.

Board/column `settings` are base64 JSON objects on the wire. Omitted/empty bytes
leave settings unchanged; encoded `{}` resets them. `name`/`label` remain
required. Unknown keys, types or ranges give `SANDBOX_BAD_SETTINGS`.

| Settings object before encoding | Meaning |
|---|---|
| Board `mode:"free"|"forward"|"strict"` | Free moves, no backwards moves, or next column only |
| Column `stage:"in_work"|"success"|"failure"` | Optional semantic stage, forbidden on intake |
| Column `no_return`, `require_comment` | No return from this source; comment required into this target |
| Column `color`, `deadline_days`, `limit`, `auto_hide_days` | `#rrggbb`; ranges in §6 |

Rules apply to UI, PAT and bulk: board mode → source `no_return` → target
`limit` → target `require_comment`. A final (`success|failure`) column is
reachable from any column despite forward/strict mode, but the other rules
still apply. Leaving a final column follows the board mode. Capacity is a
hard concurrent cap. Refusals: `LEAD_TRANSITION_FORBIDDEN`,
`COLUMN_LIMIT_REACHED`, `LEAD_COMMENT_REQUIRED`. Bulk operations report
`{affected:N,skipped:[{id,reason}]}`; wrong board/ownership can be
`FORM_LEAD_NOT_FOUND`.

Events include `id,kind,from_column_id,to_column_id,from_label,to_label,comment,
actor_user_id,actor_role,at,is_spam`. Kinds are `created,moved,column_deleted,
sandbox_deleted,spam`; actor roles are 1 staff session, 2 PAT owner, 3 system.
`is_spam` is present only on spam events. Labels are snapshots; history is
removed with the lead. `column_entered_at` supports overdue checks against
`deadline_days`; older leads may lack it.

For task boards use `/docs/tasks`: `GET /sandboxes?view=tasks` selects that
kind, which never receives form leads. Default view is `leads`.

### 4.7. Choose a test mode explicitly

1. Read `/sandboxes`, then assign the form to the intended board. A dedicated
   board is still shared live CRM, not an isolated test environment.
2. **No external delivery:** use the owner's authenticated JWT dev session on
   their dev form. This stores `is_test:true` and suppresses notifications.
   Inspect it in the cabinet with test rows enabled; dev PAT cannot read leads.
3. **Delivery test:** with authorized live access and `forms:write`, call
   `POST /forms/{form_id}/test-lead {form_id}`. It stores a test lead and really
   sends notifications labelled `[ТЕСТ]` and a webhook with `test:true`.
   Verify the actual receiver and inspect the board with
   `filter.include_test=true`. `/webhooks/{id}/test` likewise contacts a real
   receiver; neither endpoint is a dry run.

Do not test a dev form by simply adding a dev PAT to `POST /form-leads`. This
public method downgrades PAT to anonymous context and resolves the live form.
It does not guarantee `is_test` or suppressed delivery. There is no complete
safe PAT-dev submit/read/delete loop; use an isolated local environment or the
confirmed owner's dev session. `include_test` only changes list filtering.

### 4.8. Notifications, webhooks and acceptance

Accepted real leads (not spam, deduped repeats or dev-preview tests) notify
`submit.targets`: email targets, or owner's email when no targets are set;
Telegram targets use chat ids or channel names. The bot connection belongs to
the shop and is configured through owner/admin session-only org Telegram
settings, not through PAT and never in the form schema. A missing bot silently
prevents Telegram delivery; the lead remains accepted. Delivery failures do not
undo storage.

`confirmToSender:true` emails the submitted email field if present. Missing
email gives no confirmation and no intake error. The daily confirmation cap
limits only confirmation mail, not the lead or seller notification.

`lead.created` payload contains `lead_id,form_id,form_name,values,page_url,utm,
created_at`; values/UTM are already objects here, not transport base64. The
explicit test-lead path also emits this event, with `test:true` and no page URL.
A receiver must not create a real deal from a test event. Subscription control
is session-only; PAT cannot manage subscriptions with forms/leads scopes.
Signature, retries and receiver deduplication by event id: `/docs/webhooks`.

Acceptance: re-read schema and active state; verify the rendered widget/opener,
required fields and visitor consent; verify anonymous public projection after
authorized publication (a new dev-only form correctly gives anonymous 404);
choose and verify test storage/delivery separately. Tell the owner which CRM
board to inspect and that test leads are hidden by default. A bare HTTP 200 or
`ok:true` proves neither storage nor notification delivery.

## 5. Silently ignored

Unknown schema keys survive storage but may have no rendering effect; preserve
them on updates. Unknown rendering enums fall back, numeric appearance values
are clamped. Unknown/non-string/service/file lead values and unsupported UTM
keys are dropped. Foreign/dead banner files resolve to an empty URL. Empty
widget form ids render no widget; reserved `vz-lead` has no handler. Both a
schema title and a heading field render twice. Disabled forms return `ok:false`;
antispam, daily-cap and sequential duplicate paths can return success without
a new normal lead. Invalid bridge messages drop with warnings; invalid pairs
and reqId are filtered as described in §4.5. Missing Telegram connection or
confirmation email prevents only that notification.

## 6. Limits

The following are existing contract limits, not raised by this documentation.
Runtime plan limits such as `forms_max` come from the shop's plan; a draft does
not bypass the form quota. Public intake rate limiting is separate from normal
API limits: respect `RATE_LIMITED` and `Retry-After`.

```json
{
  "schema_bytes":65536,"fields":64,"field_key_bytes":64,
  "field_text_bytes":255,"options_per_field":64,
  "lead_body_bytes":65536,"lead_value_characters":4096,
  "utm_value_bytes":200,"page_url_characters":2048,"user_agent_characters":512,
  "notification_targets":10,"target_bytes":255,"confirmation_text_bytes":2000,
  "forms_page_default":50,"leads_page_default":20,"page_limit_max":100,
  "export_rows":10000,"settings_bytes":4096,
  "deadline_days":[1,365],"column_limit":[1,1000],"auto_hide_days":[1,365],
  "bridge_pairs":64,"bridge_value_utf16_units":4096,"bridge_payload_bytes":65536,
  "bridge_req_id_utf16_units":128,"spam_elapsed_ms":[1,1999],
  "dedup_window_seconds":60,"daily_leads_default":500,
  "daily_confirmation_emails":200,"retention_days_default":365
}
```

Error markers: `FORM_SCHEMA_INVALID`, `FORM_VALIDATION_FAILED` (400);
`FORM_NOT_FOUND`, `FORM_LEAD_NOT_FOUND` (404); `FORM_CODE_TAKEN` (409);
`FORM_LIMIT_REACHED`, `RATE_LIMITED` (429). Invalid statuses or transport sizes
fail validation. Missing scopes and contour mismatches fail before operations.

## 7. How this was verified

Source review on 2026-10-25: API declarations, scope whitelist/sensitive scopes,
method guards and PAT contours; form schema validator; create/edit/list/public
form handlers; public intake, test submission, export and CRM transition
handlers; HTML sanitizer and form bridge parser. Checked the internal forms
workflow against these executable contracts; no internal workflow path is part
of this public API. Public routes, source/copy synchronization, area registration
and passport links are covered by discovery tests. No customer submissions,
notifications, publication or production writes were performed for this review.
