fix: universal sponsor pitch template (#361) #362
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api!362
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "361-universal-sponsor-pitch"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Replace 9 category-specific sponsor pitches with Marcus's universal template that leads with nonprofit/EIN status, national platform reach, and 150K+ monthly Instagram views. Category no longer affects pitch selection; custom_pitch override preserved.
Changes
src/basketball_api/services/sponsor_service.py— removedCATEGORY_PITCHESdict (9 entries), added singleDEFAULT_PITCHstring from Marcus's 2026-04-06 email. Simplifiedget_pitch()to returncustom_pitchif set, elseDEFAULT_PITCH.SponsorCategoryimport retained for use inlist_sponsors()andblast_sponsors()signatures.tests/test_sponsor_blast.py— replacedCATEGORY_PITCHESimport withDEFAULT_PITCH. Updatedtest_returns_category_default_when_no_customto assert universal pitch content. Replacedtest_all_nine_categories_have_pitcheswithtest_pitch_ignores_categorythat verifies all 9 categories return the sameDEFAULT_PITCH.Test Plan
pytest tests/test_sponsor_blast.py -v— 9/9 passedruff checkandruff format --check— cleanReview Checklist
Related Notes
None.
Related
Closes #361
PR #362 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy -- applying Python/FastAPI domain checks.
Simplification is sound. Removing 9 category-specific pitches in favor of one universal template is a clean refactor. Net -41 lines.
get_pitch()logic is now trivially correct: returncustom_pitchif set, elseDEFAULT_PITCH. TheSponsorCategoryimport is retained for signatures that still need it -- no dead import.Tests are well-structured. Three unit tests cover:
The
test_pitch_ignores_categorytest properly creates distinct sponsors per category (uniquebusiness_nameandemail) to avoid DB constraint collisions. Good.Ruff compliance: PR body states
ruff checkandruff format --checkpassed clean.BLOCKERS
{{business_name}}placeholders will render as literal text in sent emails.DEFAULT_PITCHcontains two occurrences of{{business_name}}. Theblast_sponsors()function (from PR #330) passes the pitch toload_email_template()in this dict:load_email_template()iterates over the dict in insertion order, callinghtml.replace(f"{{{{{key}}}}}", str(value))for each key. Sincebusiness_nameis processed BEFOREpitch, the replacement sequence is:{{business_name}}in the original HTML template gets replaced (correct){{pitch}}gets replaced, injecting the pitch text that contains{{business_name}}into the HTML{{business_name}}strings now embedded in the HTML from step 2 are NEVER replaced -- thebusiness_namekey was already iterated pastResult: emails will contain literal
{{business_name}}instead of the sponsor's actual business name.Additionally, the plain-text fallback (
body=pitch) sends rawDEFAULT_PITCHwith no substitution at all --{{business_name}}will always appear as literal text there too.Fix options (pick one):
get_pitch(), doreturn DEFAULT_PITCH.replace("{{business_name}}", sponsor.business_name)before returning.pitchcomes beforebusiness_name-- but this is fragile and does not fix the plain-text fallback..replace()inblast_sponsors()after callingget_pitch().Option (A) is cleanest --
get_pitch()already receives the sponsor object.This is a BLOCKER because it causes user-visible broken email content in a production outreach system. The old
CATEGORY_PITCHESdid not contain{{business_name}}placeholders, so this is a regression introduced by this PR.NITS
The test
test_pitch_ignores_categorycould add an assertion that{{business_name}}is present inDEFAULT_PITCHto document the template variable expectation, or better yet, assert thatget_pitch()output for a given sponsor does NOT contain{{business_name}}(once the fix above is applied).Minor: the escaped newlines in
DEFAULT_PITCHuse\\n(literal backslash-n in Python string). This is correct for the plain-text body rendering, just noting it is intentional.SOP COMPLIANCE
361-universal-sponsor-pitchmatches issue #361#316(sponsor outreach system) and#325(blast endpoint)sponsor_service.pyandtest_sponsor_blast.pymodifiedPROCESS OBSERVATIONS
{{business_name}}substitution bug would cause broken emails to real sponsors. This is a production outreach system with blast capability. The fix is straightforward but must be applied before merge.VERDICT: NOT APPROVED
One blocker:
{{business_name}}template placeholders inDEFAULT_PITCHwill not be substituted by the currentload_email_template()flow due to dict iteration order. Emails will contain literal{{business_name}}text. Fixget_pitch()to perform the substitution, update tests to verify, and re-submit.QA caught that {{business_name}} in DEFAULT_PITCH would render as literal text because load_email_template processes business_name before pitch in the substitution loop. Now get_pitch() resolves the placeholder itself using sponsor.business_name before returning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>PR #362 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy (basketball-api).
sponsor_service.py changes:
CATEGORY_PITCHESdict (9 entries) correctly removed, replaced with singleDEFAULT_PITCHstring.DEFAULT_PITCHcontent matches Marcus's 2026-04-06 template: opens with nonprofit/EIN status, references national platform + 150K+ monthly Instagram views, includes support categories (tournament fees, gym rentals, gear), sponsorship types (financial, product, food vouchers, community), and return visibility (logo placement, social media, brand exposure, family network). Content is accurate to the issue spec.get_pitch()correctly returnscustom_pitchif truthy, elseDEFAULT_PITCH. The{{business_name}}substitution via.replace()is applied to whichever pitch is selected -- this means custom pitches containing{{business_name}}will also get substitution, which is a reasonable and intentional behavior (documented in the docstring).SponsorCategoryimport retained for use inlist_sponsors()andblast_sponsors()signatures -- no dead import.test_sponsor_blast.py changes:
CATEGORY_PITCHESimport removed,DEFAULT_PITCHnot imported (tests verify behavior, not the constant directly). Good practice.test_returns_custom_pitch_when_set-- verifies custom_pitch override still works. Passes string without{{business_name}}, gets it back unchanged.test_returns_default_pitch_when_no_custom-- verifies universal pitch content ("registered nonprofit organization", "150,000+ views per month"), confirms{{business_name}}is resolved and literal placeholder is absent. Solid.test_pitch_ignores_category-- iterates all 9SponsorCategoryvalues with samebusiness_name, asserts all return identical pitch. Correctly uses uniqueemailper sponsor to avoid DB constraint violations. Replaces the oldtest_all_nine_categories_have_pitches.test_business_name_substituted_in_default-- explicit test for{{business_name}}resolution with "Joe's Pizza". Covers the key substitution behavior.4 tests covering: custom override, default content, category independence, and placeholder substitution. Coverage is thorough.
BLOCKERS
None.
NITS
{{business_name}}in custom_pitch is a silent behavior change. The oldget_pitch()returnedcustom_pitchverbatim. The new one runs.replace("{{business_name}}", ...)on it. If any existingcustom_pitchvalues in the database contain the literal string{{business_name}}, they will now get substituted. This is almost certainly the desired behavior (and the docstring documents it), but there is no test for a custom_pitch that contains{{business_name}}. Consider adding one to lock in this behavior explicitly.Escaped unicode in string literals --
\\u2014(em dash) and\\u2022(bullet) are used throughoutDEFAULT_PITCH. These work correctly in Python string concatenation, but actual unicode characters would be more readable. Non-blocking style preference.SOP COMPLIANCE
361-universal-sponsor-pitchPROCESS OBSERVATIONS
Clean, focused change. Net -30 lines (76 additions, 106 deletions) -- simplification that removes category complexity in favor of a single proven template. Low change failure risk since the blast endpoint behavior is simplified, not expanded. The
_make_sponsorhelper acceptingbusiness_nameandemailkwargs implies good test fixture design that predates this PR.VERDICT: APPROVED