Expand Salesforce sync to SchoolClass, ClassTeacher, and Lesson#851
Draft
fspeirs wants to merge 6 commits into
Draft
Expand Salesforce sync to SchoolClass, ClassTeacher, and Lesson#851fspeirs wants to merge 6 commits into
fspeirs wants to merge 6 commits into
Conversation
Mirrors three more Salesforce objects in the salesforce_connect schema: Classroom__c, Lesson__c, and Contact_Classroom_Affiliation__c. Each model inherits from Salesforce::Base and exposes the external-ID column used by later sync jobs. Factories are added for spec coverage. Subsequent commits wire the sync jobs and model callbacks that write to these mirrors. Co-authored-by: Cursor <cursoragent@cursor.com>
The parent-sync race guard introduced for RoleSyncJob in #850 is needed by every job that resolves a Salesforce parent via an __r__<external_id_field> lookup. Move the helper (and its comment) onto Salesforce::SalesforceSyncJob so subsequent jobs (SchoolClass, Lesson, ClassTeacher) can call it without duplicating the implementation. RoleSyncJob now inherits the behaviour. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds three new GoodJob-backed sync jobs that mirror each model to its Heroku Connect counterpart: - Salesforce::SchoolClassSyncJob -> Classroom__c - Salesforce::ClassTeacherSyncJob -> Contact_Classroom_Affiliation__c - Salesforce::LessonSyncJob -> Lesson__c Each job inherits the parent-sync race guard from SalesforceSyncJob and calls ensure_parent_synced! before saving to defer writes until the parent record has landed in Salesforce. Backfill rake tasks are added under the existing salesforce_sync namespace so operators can replay each sync after deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
…SchoolProject
Wires the new sync jobs to the domain events that affect their mirror rows:
- SchoolClass after_commit (create/update) -> SchoolClassSyncJob
- Lesson after_commit (create/update, classroom lessons only) -> LessonSyncJob
- ClassTeacher after_commit (destroy) -> SchoolClassSyncJob (refresh members)
- ClassTeacher after_commit (create on an existing class)
-> SchoolClassSyncJob + ClassTeacherSyncJob
- SchoolProject state transitions in/out of submitted/complete
-> LessonSyncJob (refresh submitted-projects count)
The ClassTeacher create callback is gated by EXISTING_CLASS_THRESHOLD so it
fires only when the parent SchoolClass was created in a prior transaction;
brand-new classes are handled by the SchoolClass after_commit, and initial
teacher affiliations are intentionally left to the salesforce_sync:class_teacher
backfill task.
Adds spec/support/negated_matchers.rb (a shared not_have_enqueued_job
matcher used by these specs and by the class_student spec in the next commit).
Co-authored-by: Cursor <cursoragent@cursor.com>
A Lesson's numberofassignedprojects__c reflects lesson.remixes.count, which only changes when a student creates a remix project for the lesson — not when students join or leave the class. Hook the LessonSyncJob enqueue on Project after_commit (on: :create, with remixed_from_id present) so the sync fires at the actual point of change. ClassStudent's after_commit therefore only enqueues SchoolClassSyncJob (to refresh Classroom__c.numberofmembers__c); the per-lesson enqueue is gone, which avoids the O(lessons-per-class) job storm on every roster change. Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a Salesforce/Heroku Connect section covering:
- that sync targets the salesforce_connect DB (Heroku Connect mirror),
not the Salesforce REST API
- the SALESFORCE_ENABLED feature flag
- the canonical post-deploy backfill order (school, role, contact,
school_class, class_teacher, lesson)
- the parent-sync race guard: when to call ensure_parent_synced!,
where the helper lives, and the retry_on behaviour that makes it
self-heal
Also records the spec env requirement (SALESFORCE_CONNECT_DB + matching
Heroku Connect tables) under Testing.
Co-authored-by: Cursor <cursoragent@cursor.com>
Test coverageSimpleCov coverage data was unavailable for this run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends the Heroku Connect Salesforce sync pattern from PR #677 to three more models:
Classroom__cContact_Classroom_Affiliation__cLesson__cAdds new mirror models, sync jobs, model/state-machine callbacks, and backfill rake tasks, and generalises the parent-sync race guard introduced in #850 onto the
Salesforce::SalesforceSyncJobbase class so every job that uses__r__<external_id>lookups gets it for free.What's in each commit
Salesforce::SchoolClass / Lesson / ClassTeacher(subclasses ofSalesforce::Base) plus factories. No behaviour change yet.ensure_parent_synced!ontoSalesforceSyncJobbase — move the helper added in Guard Salesforce::RoleSyncJob against missing parent records #850 out ofRoleSyncJoband onto the base class.RoleSyncJobkeeps its call sites unchanged.ensure_parent_synced!for their parent) andsalesforce_sync:school_class / :class_teacher / :lessonrake tasks.after_commithooks for the three models, plusSchoolProjectStateMachinetransitions in/out of:submittedand:completeto keepLesson__c.numberofsubmittedprojects__cfresh. Includes a sharednot_have_enqueued_jobmatcher underspec/support/.LessonSyncJobfrom remix creation, not class-roster changes —numberofassignedprojects__creflectslesson.remixes.count, which only changes when a remixProjectis created. Hook the enqueue onProject after_commit :create(remixed_from_id.present?) so the sync fires at the actual change point, and remove the O(lessons-per-class) enqueue fromClassStudent.AGENTS.md— env, feature flag, backfill order, and the parent-sync race-guard pattern.ClassTeacher callback gating
Creating a brand-new
SchoolClasswith initial teachers fires both theSchoolClass after_commitand theClassTeacher after_commitin the same transaction. To avoid duplicateSchoolClassSyncJobs and unsequenced affiliation writes:destroy,ClassTeacheronly refreshes the classroom (SchoolClassSyncJob).create,ClassTeacherenqueues bothSchoolClassSyncJobandClassTeacherSyncJobonly if the parent class was created in a prior transaction (school_class.created_at < EXISTING_CLASS_THRESHOLD.ago).salesforce_sync:class_teacherbackfill task.Test plan
docker compose run --rm api rspec spec/jobs/salesforce spec/models/{school_class,lesson,class_student_salesforce_sync,class_teacher_salesforce_sync,project_salesforce_sync}_spec.rbdocker compose run --rm api bundle exec rubocopSALESFORCE_ENABLED=true, run backfills in order:rails salesforce_sync:school salesforce_sync:role salesforce_sync:contact salesforce_sync:school_class salesforce_sync:class_teacher salesforce_sync:lessonsalesforce_connect: every parent row has a non-nilsfidbefore child writes; failed rows should self-heal viaretry_on SalesforceRecordNotFound.Notes / follow-ups (not in this PR)
retry_on SalesforceRecordNotFoundexhausts; happy to add a follow-up.salesforce_sync:allconvenience task to run the canonical backfill order in one go.Made with Cursor