Repository navigation
Move contract setup out of the management commands into b2b.contracts (C3 2/3) - #3964
Merged
Merged
Conversation
OpenAPI ChangesShow/hide changesUnexpected changes? Ensure your branch is up-to-date with |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Forced moves retain old contract membership, and the direct removal helper can destructively modify unrelated runs.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Extracts B2B contract provisioning into reusable services for management commands and the upcoming staff API.
Changes:
- Adds reusable contract, courseware, variant, removal, and code-count helpers.
- Updates management commands to use the shared logic.
- Corrects enrollment-code documentation and adds service tests.
File summaries
| File | Description |
|---|---|
b2b/contracts.py |
Adds shared contract provisioning services. |
b2b/contracts_test.py |
Tests the new services. |
b2b/models.py |
Supports organization prefixes for program runs. |
b2b/api.py |
Corrects enrollment-code documentation. |
b2b/management/commands/b2b_contract.py |
Uses shared contract creation. |
b2b/management/commands/b2b_courseware.py |
Uses shared add/remove services. |
b2b/management/commands/b2b_codes.py |
Uses product-based expected code counts. |
b2b/management/commands/check_contract_variant.py |
Uses shared default-variant creation. |
docs/source/b2b/commands.md |
Updates command behavior documentation. |
docs/source/b2b/orgs_contracts.md |
Clarifies code-refresh behavior. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 15, 2026 14:53
32c3b85 to
de77cc6
Compare
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 17, 2026 14:43
d92f623 to
135ebb3
Compare
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 17, 2026 14:48
135ebb3 to
9ebf948
Compare
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 17, 2026 15:23
9ebf948 to
d24d23c
Compare
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 17, 2026 18:14
fa9b12a to
1847f20
Compare
jkachel
approved these changes
Sep 18, 2026
jkachel
left a comment
Contributor
There was a problem hiding this comment.
LGTM - did try running the commands and (other than the argument order being wrong for b2b_courseware add; order is weird when using subparsers) everything worked as expected.
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 22, 2026 13:51
1847f20 to
acb8d4c
Compare
… (C3 2/3) The staff contract API needs the same create, courseware add/remove and code-count logic the commands carry inline, so it lives in one module both call. Behaviour that changes on the way: - create_contract adds the default variant set. Command-created contracts had none, and get_all_variant_runs returns nothing without one, so check_contract_variant --fix-default was a required follow-up step. b2b_contract create also prints the new contract's ID. - Programs added through b2b_courseware follow the organization's courseware prefix, as courses already did. add_program_courses never passed a prefix, so program runs always got UAI_. Other callers of add_program_courses keep that default. - b2b_courseware remove only touches runs that are in the contract. Naming a run outside it used to close the run and deactivate its products. - b2b_codes validate counts expected codes per active product rather than per linked run, matching what ensure_enrollment_codes_exist creates. A removed run kept linked by its enrollments has an inactive product, so the per-run count never matched and validate re-ran every time. Also corrects the docs and the create_contract_run comment that said saving a contract generates enrollment codes. ContractPage.save only sets the title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUA1EjC95LZV4J5swhZgxy
…ng to remove remove_run_from_contract closes a run, deactivates its products and deletes codes without checking that the run is in the contract. Its only caller, remove_courseware_from_contract, filters to the contract's runs first, so the helper is now private and that check stays in one place. b2b_courseware remove printed nothing for a course or run with no runs in the contract, where the old command reported how many runs it found. It now says nothing was removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUA1EjC95LZV4J5swhZgxy
Copilot flagged that force changed the legacy FK but only added to the M2M, so the run stayed in its previous contract as well. jkachel, who added the flag, says it was meant to re-point a run when something went wrong, that run membership can be adjusted elsewhere, and that the many-to-many needs a different mechanism anyway, so it should go. A run already in another contract is now always left where it is and reported as skipped. That also matches where contracts are heading: pdpinch notes that letting public runs into contracts requires one run to appear in several of them, which is the opposite of a move. The remaining questions about run-to-contract membership are tracked in witan as tk-decide-how-a-course-run-belongs-to-contracts-sha-a00239. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUA1EjC95LZV4J5swhZgxy
…ry line add_program_courses returns a 2-tuple; the docstring said three. The remove subcommand's refactor dropped the course-level summary message that the pre-refactor command printed, leaving only per-run lines. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W2XzZbf9sYVFaw6PVaw7uX
blarghmatey
force-pushed
the
b2b-c3-contract-services
branch
from
September 25, 2026 16:38
acb8d4c to
b9c83c4
Compare
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.
What are the relevant tickets?
Capability C3 of the B2B onboarding RFC, https://github.com/mitodl/hq/discussions/12784. Design and decisions: https://github.com/mitodl/hq/discussions/12784#discussioncomment-18437846.
Stack (2 of 3). Based on #3963. 3/3 adds the staff contract API on top of this.
Description (What does it do?)
The staff contract API needs the create, courseware add/remove and code-count logic the management commands carry inline. It now lives in
b2b/contracts.py, and the commands call it:create_contract: the contract under its organization, plus a default variant setensure_default_variant: used bycheck_contract_variant --fix-defaultadd_courseware_to_contract: a program, course or existing run.no_reruns=Trueby default, so repeating the call doesn't mint another runremove_courseware_from_contract/remove_run_from_contractexpected_enrollment_code_countBehaviour that changes:
b2b_contract creategives the contract a default variant set. Without one,get_all_variant_runsreturns nothing, which is whycheck_contract_variant --fix-defaultwas a required follow-up. The command also prints the new contract's ID.b2b_courseware adduse the organization's courseware prefix, as courses already did.add_program_coursesnever passed a prefix, so program runs always gotUAI_. Its other callers keep that default.b2b_courseware removeonly touches runs that are in the contract. Naming a run outside it used to close the run and deactivate its products.b2b_codes validatecounts expected codes per active product rather than per linked run, matching whatensure_enrollment_codes_existcreates. A removed run kept linked by its enrollments has an inactive product, so the per-run count never matched and validate re-ran code generation every time.Also corrects
docs/source/b2b/commands.md,docs/source/b2b/orgs_contracts.mdand the comment increate_contract_run, which said saving a contract generates enrollment codes.ContractPage.saveonly sets the title. Bringing a contract's codes in line takes an explicit call:b2b_courseware add --make-codes,b2b_codes validate, orcreate_contract_run(queue_codes=True).How can this be tested?
Run in the web container:
pytest b2b, 619 passed and 1 skipped.makemigrations --checkreports no changes. pre-commit passes.b2b/contracts_test.pycovers the new functions. The existingb2b_coursewareadd and remove tests (b2b/management/tests/b2b_courseware_test.py,b2b/commands_test.py) pass unchanged. No test invokesb2b_contractorcheck_contract_variant.Not run by hand. To check the commands locally:
b2b_contract create <org_key> "Test" code --max-learners 2prints the new ID, andcheck_contract_variant <id>finds a default variantb2b_courseware add <id> <course readable_id> --make-codesb2b_codes validate --contract <id>reports matching found and expected counts🤖 Generated with Claude Code
https://claude.ai/code/session_01NUA1EjC95LZV4J5swhZgxy