# Receiving flow

The visual emits ops; this flow decides whether to apply them. Treat everything in the
envelope as untrusted input — the visual runs in the user's browser and its `submitted_by`
field is a hint, not a claim.

## Trigger

The visual has no network path of its own — it can't call a flow directly, and adding that
capability would disqualify it from certification — so every option below is some form of
handoff, not a direct call. Three ways to receive one, in the order most reports should reach
for them:

### When a new email arrives (primary)

The **Email changes** button already copies the changeset to the clipboard and opens Outlook
Web with the approver's address and a subject pre-filled; the reader pastes and sends. On the
flow side, an **Office 365 Outlook → When a new email arrives (V3)** trigger, pointed at that
same mailbox (**Format visual → Editing → Approver inbox**) and filtered on subject containing
`MilOrgChart Update ChangeSet`, picks it up with nothing else to configure on the report
author's side.

The body is the routing header (chart name, GUID, base revision, the reader's note) followed
by a marker line, then the JSON changeset the reader pasted beneath it. Split the body on
`PASTE THE EXPORTED CHANGESET BELOW THIS LINE` and **Parse JSON** everything after it, schema
taken from `schema/orgchart.schema.json` → `changeset`. Mark the email read (or move it to a
subfolder) as the last step either way, so the mailbox doubles as a queue you can see the
state of, the same as a SharePoint library would.

### When a file is created (SharePoint) — also works

Point this at whatever library the reader saves an exported file into. Switching
**Format visual → Editing → Export as** to **Download** makes the **Export changes** button
save a real file instead of copying to clipboard — named `orgchart-{chart id}-{timestamp}.json`
— which the reader then drops into the watched library themselves. No subject-line filtering
needed; the file itself is the changeset, parsed the same way (**Parse JSON** against the same
schema). This suits a workflow where changes are batched and reviewed together rather than
sent as they happen, or where email isn't the preferred handoff for some other reason.

### A Power App — for a review or approval front end

Neither of the above needs anything beyond the visual and Outlook or SharePoint, but a Power
App (embedded as a visual inside the same Power BI report, or standalone) can front either
path instead of replacing it: a text box for pasting the copied changeset, a **Submit** button
wired to a flow with a **Power Apps** trigger, and whatever review UI makes sense for your
approvers - a summary of the op count and comment before they commit to submitting, for
instance. This is the best fit if approvers want a purpose-built screen rather than reading a
changeset in an email or reviewing files in a library.

## 1. Validate the envelope

- `schema_version` matches what the flow understands. Reject on mismatch rather than
  guessing at an older shape.
- `op_count` equals `length(changeset)`.
- Every `op` is in the allowed enum. Reject the whole batch on any unknown op — partial
  application is how org charts end up with orphaned edges.

## 2. Check the revision

Recompute the content hash over the current source rows and compare to `base_revision`.
Mismatch → reject (move the file to `rejected/`, or mark the email and reply) and notify the
submitter to refresh and re-apply, rather than silently overwriting whoever saved first. The
visual can't be told directly, so the notification is the feedback channel — make it specific
about which chart and why.

## 3. Authorize

This is where edit rights are actually enforced, and it depends on which trigger received the
changeset - each hands the flow a different, equally unforgeable identity to check:

- **Email trigger**: the message's own **From** address. Authenticated by Exchange before the
  flow ever sees it, the same reasoning that made SharePoint's Created By trustworthy for the
  file-drop path - a reader pasting a changeset into an email cannot make it arrive From
  someone else.
- **File-drop trigger**: the file's SharePoint **Created By** field.
- **Power Apps trigger**: the calling user's identity, available directly as
  `User().Email` in the app without an extra lookup.

Whichever identity you have, check group membership with **Office 365 Groups → Check group
membership** or a Graph call against the group that owns the chart's UIC. The envelope itself
carries no author claim to trust - that's deliberate, since anything inside it came from the
reader's browser and is only as trustworthy as they are.

Optionally scope by UIC: an editor for `PHX01` may only touch nodes whose `uic` is `PHX01`.
Walk the changeset and reject any op targeting a node outside their scope.

Deny → reject the same way step 2 does for that trigger, and log it. Denials are the
interesting ones.

## 4. Apply, in dependency order

Ops arrive in the order the user made them, which is not the order they can be safely
applied. Sort into phases:

1. `node.create`
2. `group.create`
3. `node.update`, `group.update`
4. `assignment.create`, `assignment.update`, `assignment.end`
5. `edge.create`, `edge.update`, `edge.reparent`
6. `group_member.add`, `group_member.reorder`
7. `group_member.remove`
8. `edge.delete`
9. `group.delete`, `node.delete`

Creates before the things that reference them; deletes after the things that reference
them are gone.

Before applying `edge.create` or `edge.reparent`, walk up the proposed parent chain on
that `chain` value. If you reach the child, reject the op — a cycle in an org chart is
either a data error or a very bad day.

Wrap the whole batch in a scope with **Configure run after** → failed, so a failure jumps
to the compensation branch rather than leaving the source half-applied.

## 5. Write

Per entity, upsert into the authoritative store.

- **SharePoint lists** — one list per entity, `node_id` / `edge_id` as the key column.
  Version history and item-level permissions come free. Fastest to stand up.
- **Dataverse** — real referential integrity, real row-level security, proper delete
  behaviour on relationships. The right answer if this outlives the pilot.
- **SQL** — if the org chart already lives beside other manpower data.

Never hard-delete. Set `is_active = false` and stamp `effective_to`. A billet that
disappears from the table takes its history with it, and someone will ask what the chart
looked like at last year's inspection.

## 6. Respond and record

Mark the source item processed - the email read/filed, or the file moved to `processed/` -
and mail the submitter a confirmation naming the chart and the op count. They clear their
own draft in the visual once they see it — there's no
callback, so the confirmation is what closes the loop.

Append the full envelope plus the resolved caller to an audit list. That log is the answer
to "who moved this billet and when", which is the question that will actually get asked.

## 7. Notify

Post to the Teams channel that owns the chart: who changed what, with a link back to the
report. Keeps edits visible without anyone having to watch a list.

---

## Envelope changes since this outline was written

The envelope now carries two fields the flow should use:

```json
{
  "schema_version": "...",
  "chart_id": "UNIT_PHX",
  "submitted": "2026-08-31T14:02:11Z",
  "base_revision": "<revision the draft was built against>",
  "comment": "Stood up N7; moved the CFL under N4",
  "op_count": 7,
  "changeset": [ ... ]
}
```

- **`comment`** is the submitter's note for the approver. Surface it in the approval card;
  it is free text from the browser, so treat it as untrusted display content.
- **`base_revision`** is the staleness hook. Compare it against the source's current
  revision and **reject the batch if they differ**, telling the submitter to refresh and
  resubmit. The visual detects the same condition and warns, but it cannot refuse a refresh -
  only the flow can refuse a write.

## Ops the flow must handle

Nineteen op types, all of the form `{ op, target_id, before, after }`:

| Family | Ops |
|---|---|
| Nodes | `node.create`, `node.update`, `node.delete` |
| Edges | `edge.create`, `edge.update`, `edge.delete`, `edge.reparent` |
| Groups | `group.create`, `group.update`, `group.delete` |
| Membership | `group_member.add`, `group_member.remove`, `group_member.reorder` |
| Assignments | `assignment.create`, `assignment.update`, `assignment.end` |
| Roster | `roster.create`, `roster.update` |
| Draft | `orgchart.draft` *(visual-local; ignore)* |

Two matter especially:

- **`roster.create`** is new. The `target_id` is an **EDIPI (10 digits) or FASCN (16)**, and
  the visual validates that before emitting - but validate it again, because the flow is the
  security boundary and the visual is not. This op exists so a billet can be filled before
  the authoritative source has the person; the flow should decide whether that creates a
  provisional record or is queued for a human.
- **`assignment.create` / `assignment.update`** now carry `assigned_date` and `prd`
  separately from `effective_from` / `effective_to`. **`prd` is a projection, not an end
  date** - do not write it into a validity column, or everyone whose PRD has passed will
  silently drop out of the chart. (That exact mistake was made and caught in development.)

## Clearing a field

A patch clears a field with **`null`**, not by omitting it - `JSON.stringify` drops
`undefined` keys, so an omitted key means "unchanged" and `null` means "set to empty". The
flow must preserve that distinction or clearing a value will silently no-op.

## Idempotency

`applyChangeset` in the visual is idempotent, and the flow should be too: the same batch may
arrive twice if a submitter retries. Keying on `op_id` is the simplest guard. The visual also
reconciles on refresh - ops whose end state already matches the data are dropped from the
draft automatically - so a duplicate apply is recoverable rather than corrupting.
