Cross-references the Civi-side cutover checklist from the deploy doc so anyone deploying knows the CiviCRM prerequisites are tracked in a single place.
279 lines
9.4 KiB
Markdown
279 lines
9.4 KiB
Markdown
# Production cutover guide
|
|
|
|
Step-by-step CiviCRM-side checklist to recreate the Co-op Survey
|
|
environment on a new CiviCRM (e.g. when moving from
|
|
`client.crm.fci.coop` to production `crm.fci.coop`). The Next.js app
|
|
deploy is covered separately in [`DEPLOYMENT.md`](./DEPLOYMENT.md) (Render)
|
|
and [`AMPLIFY_DEPLOY.md`](./AMPLIFY_DEPLOY.md) (AWS Amplify). This
|
|
document captures everything that must exist *inside CiviCRM* for those
|
|
deployments to work.
|
|
|
|
Keep this file current — every time we discover a manual Civi-side
|
|
prerequisite, add it here so the next environment cutover is a single
|
|
read-through.
|
|
|
|
---
|
|
|
|
## 0. Prerequisites on the target CiviCRM
|
|
|
|
- CiviCRM 5.50+ on PHP 7.4 or 8.x.
|
|
- A service-user contact with an **API key** generated (Contact view →
|
|
edit → API Key field). The same user's permissions are what the app
|
|
runs as.
|
|
- The site's `CIVICRM_SITE_KEY` from `civicrm.settings.php`.
|
|
- If CiviCRM sits behind webserver-level Basic Auth, credentials for it.
|
|
|
|
These three (`CIVI_BASE_URL`, `CIVI_API_KEY`, `CIVI_SITE_KEY`) are the
|
|
core env vars the app needs.
|
|
|
|
---
|
|
|
|
## 1. Activity type and custom field groups
|
|
|
|
The form writes one activity per submission, of type
|
|
**`Check-in (organizing)`** (machine name, exact spelling).
|
|
|
|
That activity type must exist with the **six** custom field groups
|
|
attached, all extending `Activity` filtered to this single activity
|
|
type:
|
|
|
|
| Custom group machine name | Stage |
|
|
|----------------------------------|-------|
|
|
| `Check_in_data__organizing_` | 0 (always-on survey data + the `Stage` snapshot field) |
|
|
| `Stage_1` | 1 — Convene & Prepare |
|
|
| `Stage_2` | 2 — Grow & Plan |
|
|
| `Stage_3` | 3 — Connect & Gather |
|
|
| `Stage_4` | 4 — Excite & Build |
|
|
| `Stage_5` | 5 — Fulfill & Stabilize |
|
|
|
|
Field names *within* those groups are referenced explicitly by
|
|
`config/form.ts` (search for `civiField:`). If you renamed any field on
|
|
the source CRM, mirror the change here or update the config.
|
|
|
|
**Sanity check** after import: `Activity → Search` for any existing
|
|
`Check-in (organizing)` row and confirm every custom field renders.
|
|
|
|
---
|
|
|
|
## 2. The `Stage` custom field default — **must be cleared**
|
|
|
|
Custom field id 242 (label "Stage", inside the
|
|
`Check_in_data__organizing_` group) **must have an empty default
|
|
value**. The form is intentionally not the authority on stage; staff
|
|
set Stage on their own stage-change check-ins, and `/api/data` derives
|
|
the org's current stage from the most recent stage-bearing activity.
|
|
|
|
If a default is set (the original config had "Unknown" as the default),
|
|
every form-submitted activity gets stamped with that value and
|
|
contaminates the stage-derivation logic.
|
|
|
|
The Civi admin UI's dropdown does **not** allow you to select "blank",
|
|
so clear it via API or SQL:
|
|
|
|
**Option A — API (preferred):**
|
|
|
|
```bash
|
|
cv api4 CustomField.update \
|
|
where='[["id","=",242]]' \
|
|
values='{"default_value":null}'
|
|
```
|
|
|
|
Or in the API Explorer v4 (`Support → Developer → API Explorer v4`):
|
|
|
|
```
|
|
Entity: CustomField
|
|
Action: update
|
|
where: [["id","=",242]]
|
|
values: {"default_value": null}
|
|
```
|
|
|
|
**Option B — direct SQL** (only if the API isn't handy):
|
|
|
|
```sql
|
|
UPDATE civicrm_custom_field SET default_value = NULL WHERE id = 242;
|
|
```
|
|
|
|
Then `cv flush` or `Administer → System Settings → Cleanup Caches`.
|
|
|
|
**Cleanup of historical bad data** (optional, only if production was
|
|
running with the bad default for a while):
|
|
|
|
```bash
|
|
cv api4 Activity.update \
|
|
where='[["activity_type_id:name","=","Check-in (organizing)"],
|
|
["Check_in_data__organizing_.Stage","=","Unknown"]]' \
|
|
values='{"Check_in_data__organizing_.Stage": null}'
|
|
```
|
|
|
|
Verify: pull one activity back and confirm Stage is null:
|
|
|
|
```bash
|
|
cv api4 Activity.get \
|
|
where='[["activity_type_id:name","=","Check-in (organizing)"]]' \
|
|
select='["id","Check_in_data__organizing_.Stage"]' \
|
|
limit=5
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Framework Stage option group on Organizations
|
|
|
|
Each Organization contact has a custom field that holds its current
|
|
Framework Stage. The option group backing it (referenced by
|
|
`STAGE_OPTION_GROUP_ID` in `config/form.ts`, currently `75`) must
|
|
contain these six values, spelled exactly:
|
|
|
|
- `Inquiry`
|
|
- `Organizing`
|
|
- `Feasibility`
|
|
- `Business feasibility`
|
|
- `Store Implementation`
|
|
- `Stabilize newly opened co-op`
|
|
|
|
If the option group id differs in the target CRM, update
|
|
`STAGE_OPTION_GROUP_ID` in `config/form.ts` and redeploy.
|
|
|
|
---
|
|
|
|
## 4. Relationship type: form-filler → organization
|
|
|
|
The form resolves "what org is this submission about?" by looking up
|
|
the form-filler's **`Primary Contact`** relationship to an
|
|
Organization. The relationship type must exist with:
|
|
|
|
- `name_a_b` = `Primary Contact`
|
|
- side A = Individual
|
|
- side B = Organization
|
|
|
|
This is the value of `FORM_CONTACT_RELATIONSHIP` in `config/form.ts`.
|
|
If you use a different relationship type in the target CRM, update that
|
|
constant and redeploy.
|
|
|
|
**Per-org setup** (this is the recurring operational step): for each
|
|
Organization that should receive a survey, create exactly one *active*
|
|
`Primary Contact` relationship from the staff/board contact who will
|
|
fill out the form to that Organization.
|
|
|
|
- Zero active relationships → the form returns
|
|
`No active "Primary Contact" relationship found for your contact.`
|
|
- More than one active relationship → the form refuses with
|
|
`multiple active … relationships; staff must resolve before this link
|
|
will work.` (Deactivate the older ones.)
|
|
|
|
---
|
|
|
|
## 5. Install the `webform-mw` Civi extension
|
|
|
|
The extension adds an **Engagement Report** tab to Organization
|
|
contact pages that embeds the staff report in an iframe.
|
|
|
|
1. Copy `WebForm-mw/civi-extension/webform-mw/` to the CRM's
|
|
`[civicrm.extensionsDir]` (usually
|
|
`<civi-root>/sites/default/ext/`). The directory must be named
|
|
exactly `webform-mw` (matches `<key>` in `info.xml`).
|
|
|
|
2. `Administer → System Settings → Extensions → Add new → Refresh`,
|
|
then **Install** next to "WebForm-mw".
|
|
|
|
3. Configure — add to `civicrm.settings.php`:
|
|
|
|
```php
|
|
define('WEBFORM_MW_APP_URL', 'https://survey.fci.coop');
|
|
define('WEBFORM_MW_STAFF_KEY', '...same value as STAFF_REPORT_KEY in the app env...');
|
|
```
|
|
|
|
`WEBFORM_MW_STAFF_KEY` **must** equal the `STAFF_REPORT_KEY` env var
|
|
on the Next.js app. Rotate them together.
|
|
|
|
4. Confirm: open any Organization contact in CiviCRM — there should be
|
|
an **Engagement Report** tab. Click it; the staff report should
|
|
load with no nested scrollbar (the extension auto-sizes the iframe
|
|
via `postMessage`).
|
|
|
|
Full extension docs:
|
|
[`civi-extension/webform-mw/README.md`](./civi-extension/webform-mw/README.md).
|
|
|
|
---
|
|
|
|
## 6. CSP / `frame-ancestors` — app side
|
|
|
|
The Next.js app's `/staff/report` route must allow the CiviCRM origin
|
|
in its `frame-ancestors` CSP, or the iframe will refuse to render.
|
|
|
|
The build reads `CIVI_BASE_URL` and adds its origin to the CSP
|
|
automatically — so make sure `CIVI_BASE_URL` on the app deploy points
|
|
at the **production** CRM origin (`https://crm.fci.coop`), not
|
|
`client.crm.fci.coop`.
|
|
|
|
Confirm after deploy:
|
|
|
|
```bash
|
|
curl -sI https://survey.fci.coop/staff/report | grep -i content-security-policy
|
|
```
|
|
|
|
Should include `frame-ancestors 'self' https://crm.fci.coop` (or
|
|
whatever your production CRM origin is).
|
|
|
|
---
|
|
|
|
## 7. App env vars (cross-reference)
|
|
|
|
The full list lives in [`DEPLOYMENT.md`](./DEPLOYMENT.md#first-time-render-setup).
|
|
Production-specific reminders:
|
|
|
|
- `CIVI_BASE_URL` — production CRM, not the client/staging one.
|
|
- `STAFF_REPORT_KEY` — long random secret, identical to the
|
|
`WEBFORM_MW_STAFF_KEY` in `civicrm.settings.php` (step 5).
|
|
- `HEALTH_TOKEN` — set this in production, or `/api/health` returns
|
|
404. Use `openssl rand -hex 32`.
|
|
- Confirm none of the three core `CIVI_*` vars are missing — the app
|
|
refuses to start in `NODE_ENV=production` if any are unset.
|
|
|
|
---
|
|
|
|
## 8. Post-deploy verification
|
|
|
|
Run these against the production deploy:
|
|
|
|
1. `curl https://survey.fci.coop/healthz`
|
|
→ `{"ok":true,"service":"coop-checkin"}`
|
|
|
|
2. `curl 'https://survey.fci.coop/api/health?token=$HEALTH_TOKEN'`
|
|
→ all checks green. This validates Civi connectivity, the
|
|
`Primary Contact` relationship type, the `Check-in (organizing)`
|
|
activity type, all six custom groups, the Stage option group, and
|
|
the option-group ID match.
|
|
|
|
3. Pick a test Individual contact that has a `Primary Contact`
|
|
relationship to a test Organization. Generate a Civi checksum for
|
|
that Individual and load:
|
|
`https://survey.fci.coop/?cid=<id>&cs=<checksum>` — the form should
|
|
load with prefill.
|
|
|
|
4. Submit a stub answer. Then in Civi, pull the new activity:
|
|
|
|
```bash
|
|
cv api4 Activity.get \
|
|
where='[["activity_type_id:name","=","Check-in (organizing)"]]' \
|
|
orderBy='{"id":"DESC"}' limit=1 \
|
|
select='["id","subject","Check_in_data__organizing_.Stage"]'
|
|
```
|
|
|
|
- `Stage` must be **null** (not "Unknown"). If it's "Unknown",
|
|
step 2 above wasn't completed on this CRM.
|
|
- `subject` should be `Co-op Survey (form submission)`.
|
|
|
|
5. Open the target Organization in CiviCRM → **Engagement Report**
|
|
tab. The staff report should render the submission you just made.
|
|
|
|
---
|
|
|
|
## Change log
|
|
|
|
When you discover a new manual prerequisite, append a short note here
|
|
so the rationale survives.
|
|
|
|
- **2026-06-08** — Documented the field-242 "Unknown" default issue
|
|
after a production submission was stamped `Stage = "Unknown"`.
|
|
Cleared via `CustomField.update`; see step 2.
|