Keep an affiliate's removal sticky so they are not notified again - #7790
Conversation
…otified again A direct affiliation ended because the person asked us to leaves no record on the row, so a later re-add is indistinguishable from a first-time add and mails the person again. Record the request on the removed row (existing flags column, no migration) and skip the three here-is-your-affiliation mails for any later row for the same seller and person.
|
| def mark_removed_at_request! | ||
| update!(removed_at_request: true) | ||
| end |
There was a problem hiding this comment.
mark_removed_at_request! is never called by the production self-removal flow. Products::AffiliatedController#destroy soft-deletes the affiliation by updating only deleted_at and updated_at, so the new flag is not recorded. If the seller later re-adds that affiliate, reassignment_blocked? remains false and the invitation and product-update emails are sent again. The tests miss this because they set the flag directly instead of exercising the real removal flow.
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/models/direct_affiliate.rb
Line: 160-162
Comment:
**Removal Flag Is Never Set**
`mark_removed_at_request!` is never called by the production self-removal flow. `Products::AffiliatedController#destroy` soft-deletes the affiliation by updating only `deleted_at` and `updated_at`, so the new flag is not recorded. If the seller later re-adds that affiliate, `reassignment_blocked?` remains false and the invitation and product-update emails are sent again. The tests miss this because they set the flag directly instead of exercising the real removal flow.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.A legacy row that no longer passes today's rules (commission out of range, no destination URL) raises on update!, so the request went unrecorded on exactly the rows the removal path was built to handle. update_all with the flag setter mirrors the removal itself.
Stages
What
Ending a direct affiliation because the affiliate asked to be removed left no record on the row: a
soft-deleted
DirectAffiliateis indistinguishable from one that never existed. So when the sameperson is added again — by the seller in the dashboard, or by a bulk pass —
AffiliateMailermailsthem again as if it were a first-time add, and there is nothing to point at when someone says
"please remove me and do not add me again".
Fix
removed_at_requestflag onAffiliate, using the existingaffiliates.flagscolumn (bit 4, nomigration).
DirectAffiliate#mark_removed_at_request!records it on the row being removed; the flag survivesthe soft delete, because nothing in the model hides deleted rows.
DirectAffiliate#reassignment_blocked?is true when any row for the same(
seller_id,affiliate_user_id) carries it, so it holds across a re-add and tells a repeatrequester apart from a first-time one.
AffiliateMailerreturns early for the three "here is your affiliation" mails when it is true:direct_affiliate_invitation,notify_direct_affiliate_of_updated_products,notify_direct_affiliate_of_new_product.Deliberately not changed, so a reviewer can disagree with the scope rather than discover it:
request is honoured by not mailing them.
person opting out. Suppressing on it would silently drop people from later launches.
earn.
Before / After
Backend-only, no rendered surface: the observable is the mailer's return value, asserted as
ActionMailer::Base::NullMailin the specs below.Test Plan
CI=1 bin/bundle exec rspec spec/models/direct_affiliate_spec.rb spec/mailers/affiliate_mailer_spec.rbLocal: 101 examples, 7 failures — the same 7 as
origin/mainin a fresh worktree of the samecommit range (92 examples, 7 failures):
#notify_affiliate_of_sale(5),#product_sales_infoand#products_data, all unrelated to this diff and failing without it. Everything this PR touchespasses, including the 9 new examples (6 model, 3 mailer). rubocop on the five touched files: no
offenses.
QA
DirectAffiliate#reassignment_blocked?tofalsefails 4 of the 7new examples (
7 examples, 4 failures) — the three that still pass are the negative cases (anotherseller, another person, nobody removed); (b) reverting
mark_removed_at_request!toupdate!fails the legacy-row example (
1 example, 1 failure). The specs bite.evidence; nothing to screenshot.
Premerge review: clean @ 273afe2 — panel, codex
gpt-6-astra(thinking high) 0 findings, claude
claude-fable-5-10 findings,overall: patch is correct (0.98).Risk Assessment
Low — on every path this can only remove a recipient from three mails; no send becomes newly
possible, and no seller-visible behaviour changes. The blast radius is affiliate mail to
(seller, person) pairs that someone explicitly recorded as asked-to-be-removed.
AI disclosure
🤖 Generated with deepseek-v4.1-flash (DeepSeek, via OpenRouter), running an autonomous
product-development session.
Prompts/instructions given: the standing autonomous product-dev workflow, applied to the observed
behaviour above; all code, tests and this body were written by the agent.