Add PRODUCTION_CUTOVER.md and link from DEPLOYMENT.md
Cross-references the Civi-side cutover checklist from the deploy doc so anyone deploying knows the CiviCRM prerequisites are tracked in a single place.
This commit is contained in:
@@ -3,6 +3,12 @@
|
||||
This app is built to deploy to **[Render](https://render.com)** as a Web
|
||||
Service via the included `render.yaml` blueprint.
|
||||
|
||||
For the **CiviCRM-side** setup that must happen before this app can
|
||||
work (extension install, custom-field defaults, relationship types,
|
||||
option groups, post-deploy verification), see
|
||||
[`PRODUCTION_CUTOVER.md`](./PRODUCTION_CUTOVER.md). Keep that doc
|
||||
current — every Civi prerequisite we discover belongs there.
|
||||
|
||||
## Pre-flight checklist
|
||||
|
||||
Before deploying, confirm:
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user