# migrate-workflow-flows

Phase 1 migration script: rewrites 19 legacy SF-Workflow / InvocableProcess
flows into native AutoLaunchedFlow format so the NexusCRM flow engine can
execute them under a single code path.

Source of truth: `amber-main-design-20260416-230159.md`, section
"Phase 1: Migration Script (unblocks everything)".

## What it does

For each of the 19 KEEP-list DeveloperNames:

1. Read `Flow.Metadata` + `FlowDefinition.ProcessType`.
2. Parse root-level `processMetadataValues` (ObjectType / TriggerType /
   ObjectVariable / OldObjectVariable).
3. Build a native `FlowStart` element (unless the flow is an
   InvocableProcess callee — those become callable-only AutoLaunchedFlows
   with no `start`).
4. Rewrite every `myVariable_current[.X[.Y...]]` token in the metadata
   tree to `$Record[.X[.Y...]]`, and `myVariable_old[.X[...]]` to
   `$Record__Old[...]`. `__r` chains pass through verbatim — the GAP 6
   runtime lazy-load resolves them.
5. Update `FlowDefinition.ProcessType` / `TriggerType` /
   `TriggerObjectOrEvent` and write the migrated JSON back to
   `flow.Metadata`, inside a single transaction per flow with
   `SELECT ... FOR UPDATE`.
6. Verify the migrated metadata roundtrips through
   marshal -> unmarshal -> `reflect.DeepEqual`.

Special cases:

- `Order_Expiration_Notice_90` — Workflow-sourced but orchestrates 3
  scheduled Waits. Migrated as AutoLaunchedFlow + `start.triggerType =
  "Scheduled"` per design doc Wave 3.
- `Order_Expiration_Alert_{7,15,30,60,90}` — InvocableProcess callees.
  Migrated as AutoLaunchedFlow with NO `start`. They remain callable by
  other flows via `actionType: "flow"`.

## Usage

```bash
# Dry run (default). Writes migration-output/<flow>/<flow>.original.json
# and .migrated.json. No DB writes.
go run ./scripts/migrate-workflow-flows \
    --output-dir=/tmp/migration-output

# Single-flow debug:
go run ./scripts/migrate-workflow-flows --flow=Order_Account_Name

# Apply to the test DB. Allow-listed by TIDB_USER prefix.
TIDB_HOST=... TIDB_USER=4Qbn3F3nyaU9DLk.root TIDB_PASSWORD=... \
TIDB_DATABASE=nexuscrm \
  go run ./scripts/migrate-workflow-flows --apply
```

Env vars required by `--apply` (and for dry-run, since it reads live
metadata from the DB):

- `TIDB_HOST`, `TIDB_PORT` (default 4000), `TIDB_USER`, `TIDB_PASSWORD`,
  `TIDB_DATABASE`.
- `TIDB_PASSWORD` is **not** required when `TIDB_HOST` is `127.0.0.1` /
  `localhost` (matches the TLS-skip rule on the same hosts) — local dev
  TiDB typically runs root with no password.

## Activation is intentionally out of scope

The script migrates metadata only. `ActiveVersionId` is cleared on every
touched FlowDefinition so the trigger listener stops dispatching until
Phase 4 re-activates waves explicitly. This neutralises the risk of a
KEEP-list flow (whose legacy ActiveVersionId was non-empty) going live
in native format the moment Lanes A/B/C merge.

**Wave 3 note — `Order_Expiration_Notice_90`:** this flow migrates to
`start.triggerType=Scheduled`, which drives off every record in
`start.object` (Invoice_Collection__c) on each scheduler tick. The
legacy root decision filters live inside `decisions` and are NOT
translated into `start.filters` by this script — decision translation
is out of scope for Phase 1. Before Wave 3 activation, a human must
either:

- port the root decision criteria into `start.filters` so the scheduler
  enumerates a narrow set, OR
- confirm via the scheduler's query plan that the Invoice_Collection__c
  row volume is small enough for per-tick full-scan evaluation.

The script fails the migration with a loud error if any of the 14
record-triggered flows is missing `ObjectType` / `TriggerType`, and
refuses unknown `TriggerType` values. Unknown inputs never turn into
an inert-but-valid flow.

## Safety

- `--apply` refuses to run unless `TIDB_USER` begins with the hard-coded
  test-DB prefix `4Qbn3F3nyaU9DLk`. Escape hatch:
  `--i-know-this-is-not-test` (logs a warning to stderr, still proceeds).
- `--dry-run` does read the live DB (there is no way to produce a useful
  migration preview without it). If you point dry-run at prod/staging
  the output directory will contain prod flow metadata — treat that
  directory like any other DB dump (do not commit, scrub on cleanup).
- Every per-flow UPDATE pair runs inside a single transaction. The
  `FlowDefinition` row is `SELECT ... FOR UPDATE`-locked for the full
  transaction; a concurrent writer cannot race our read + write.
- Idempotent: re-running `--apply` against already-migrated flows is a
  no-op (0 rows affected). Detection: `processType == "AutoLaunchedFlow"`
  AND no `myVariable_current` / `myVariable_old` tokens remain outside
  the preserved `processMetadataValues` provenance block.
- Failure isolation: one flow failing does not abort the run. Failed
  flows are logged and listed at the end.

## Tests

`go test -race ./scripts/migrate-workflow-flows/...`

Test fixtures live in `testdata/*.json` + `testdata/*.meta.json`. They
are not checked in as "generated" — re-dump them from the test DB with:

```bash
for name in $(go run ./scripts/migrate-workflow-flows --flow=__list__ 2>&1); do
  mysql -u 4Qbn3F3nyaU9DLk.root -p$PASS -D nexuscrm -N -B \
    -e "SELECT f.Metadata FROM flow f JOIN FlowDefinition fd ON f.DefinitionId=fd.Id \
        WHERE fd.DeveloperName='$name' ORDER BY f.VersionNumber DESC LIMIT 1" \
  > scripts/migrate-workflow-flows/testdata/$name.json
done
```

If a fixture is missing at test time, the per-flow sub-test `t.Skip`s
rather than fails — this keeps CI green when the full corpus has not
been dumped.

## Output format

`--dry-run` writes two files per flow under `--output-dir`:

- `<flow>.original.json` — the stored Metadata, pretty-printed.
- `<flow>.migrated.json` — the post-migration Metadata, pretty-printed.

Plus a one-line summary per flow to stdout:

```
Order_Account_Name: migrated (orig 19466B -> migrated 19638B), rewrites=5, processType Workflow -> AutoLaunchedFlow, triggerType RecordAfterSave
```

## See also

- `scripts/deactivate-obsolete-flows/` — Phase 0 (DROP list). Same
  safety-gate pattern, same env var protocol.
