Commit 9d9d042
authored
Sync Schools and Roles to Salesforce on Create and Update (#677)
## Status
- Closes
RaspberryPiFoundation/experience-cs#1677
## What's changed?
### Infrastructure
- Adds a `salesforce_connect` database configuration in `database.yml`
to connect to the `rpf-heroku-connect` Heroku Connect datastore
(read/write via a separate PostgreSQL connection, with `database_tasks:
false` so Rails migrations leave it alone).
- Adds a `salesforce_connect` Docker service (image:
`ghcr.io/raspberrypifoundation/heroku-connect`) to `docker-compose.yml`
for local development, with a named volume and a health check.
- Adds the `SALESFORCE_CONNECT_*` environment variables to
`.env.example` and `docker-compose.yml`.
- Updates CI (`.github/workflows/ci.yml`) to spin up the
`heroku-connect` service container, pass the Salesforce connection env
vars, and add `packages: read` permission so the private image can be
pulled.
### Models
- `Salesforce::Base` — abstract Active Record base class that routes all
reads and writes to the `salesforce_connect` database.
- `Salesforce::School` — maps to the `salesforce.editor__c` table (PK:
`editoruuid__c`).
- `Salesforce::Role` — maps to the
`salesforce.contact_editor_affiliation__c` table (PK:
`affiliation_id__c`).
- `Salesforce::Contact` — maps to the `salesforce.contact` table (PK:
`pi_accounts_unique_id__c`).
### Jobs
- `Salesforce::SalesforceSyncJob` — base job class that:
- Checks the `SALESFORCE_ENABLED` env var and discards the job (no
retry) if it is not `true`.
- Retries on `SalesforceRecordNotFound` with polynomial backoff, up to
10 attempts.
- Truncates string values to respect Salesforce column limits (appending
`…`).
- Enqueues onto the new `salesforce_sync` queue.
- `Salesforce::SchoolSyncJob` — upserts a `School` record into
`salesforce.editor__c`, mapping all address, status, and metadata
fields. Defaults `userorigin__c` to `'for_education'` when blank.
- `Salesforce::RoleSyncJob` — upserts non-student `Role` records into
`salesforce.contact_editor_affiliation__c`. Skips student roles
entirely.
- `Salesforce::ContactSyncJob` — looks up the Salesforce Contact by the
school creator's user ID and syncs the `creator_agree_to_ux_contact`
flag. Raises `SalesforceRecordNotFound` (triggering retry) if no Contact
exists yet.
### Model callbacks
- `School` — `after_commit` on create/update enqueues both
`SchoolSyncJob` and `ContactSyncJob`.
- `Role` — `after_commit` on create/update enqueues `RoleSyncJob`.
### GoodJob queue configuration
- Adds the `salesforce_sync:10` queue to allow up to 10 concurrent
Salesforce sync workers.
### Rake tasks
- `salesforce_sync:school` — bulk-enqueues `SchoolSyncJob` for every
School (for backfilling).
- `salesforce_sync:role` — bulk-enqueues `RoleSyncJob` for every Role
(for backfilling).
- `salesforce_sync:contact` — bulk-enqueues `ContactSyncJob` for every
School (for backfilling the UX contact flag).
### Tests
- Full RSpec coverage for `SchoolSyncJob`, `RoleSyncJob`, and
`ContactSyncJob`, including field mapping, skipping/discarding
behaviour, and error cases.
- Model specs for `School` and `Role` verify that the correct jobs are
enqueued after create/update.
## Points for consideration
- **Security**: The `salesforce_connect` connection credentials are
injected via environment variables. No secrets are committed to the
repo.
- **Performance**: Each `after_commit` callback enqueues a background
job (GoodJob), so there is no synchronous overhead on the request.
Concurrency is capped per-record to avoid TOCTOU races.
- **Salesforce Contact availability**: `ContactSyncJob` requires the
Salesforce Contact record to already exist (keyed by `creator_id`). If
it does not exist yet, the job retries up to 10 times with polynomial
backoff.
## How to Test Locally
To test these changes locally, you can:
1. Log in as a user that has no school.
2. Go to For Education > Create your school account
3. Complete the sign-up form
Observe the GoodJob dashboard - you should see new `SchoolSyncJob` and
`RoleSyncJob` jobs in the `salesforce_sync` queue. In the Rails Console,
you can also inspect the number of `Salesforce::School` and
`Salesforce::Role` objects that have been created - you should see them
increase when you add a new school.
## Steps to perform after deploying to production
After deploying, run the rake backfill tasks to sync existing data:
```
rails salesforce_sync:school
rails salesforce_sync:role
rails salesforce_sync:contact
```1 parent efc9f26 commit 9d9d042
27 files changed
Lines changed: 677 additions & 7 deletions
File tree
- .github/workflows
- app
- jobs/salesforce
- models
- salesforce
- config
- initializers
- lib
- tasks
- spec
- factories/salesforce
- jobs/salesforce
- lib
- models
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
56 | 57 | | |
57 | 58 | | |
58 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
59 | 65 | | |
60 | 66 | | |
61 | 67 | | |
| |||
74 | 80 | | |
75 | 81 | | |
76 | 82 | | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
77 | 99 | | |
78 | 100 | | |
79 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
41 | 47 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
0 commit comments