Skip to content

[T3322] FIX allow writing letters while the FCP is suspended - #352

Open
danpa32 wants to merge 1 commit into
18.0from
T3322-letter-writing-during-suspension
Open

[T3322] FIX allow writing letters while the FCP is suspended#352
danpa32 wants to merge 1 commit into
18.0from
T3322-letter-writing-during-suspension

Conversation

@danpa32

@danpa32 danpa32 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

T3322 — Sponsor cannot write a letter on MyCompassion while the FCP is suspended

Reported by Mme Ciuchi: her sponsored child has no "Write a letter" option on MyCompassion.
Requested behavior: a sponsor should be able to write a letter even while the FCP is suspended, holding it as an exception until the FCP reactivates, at which point it should be auto-submitted.

Root cause

The backend already has a complete "hold as exception, auto-resubmit on reactivation" mechanism, untouched by the v14→v18 migration:

  • recurring.contract._compute_can_write_letter (sponsorship_compassion/models/contracts.py:451-483) sets can_write_letter = False whenever project_id.hold_s2b_letters is set — unless the context key allow_during_suspension is passed (@api.depends_context("allow_during_suspension"), non-stored compute).
  • correspondence.create_commkit() (sbc_compassion/models/correspondence.py:696-719) already sets letter.state = "Exception" and holds the gmc.message as "postponed" when project_id.hold_s2b_letters is true, instead of sending it.
  • Lifecycle events already auto-trigger hold_letters_action() / reactivate_letters() on suspend/reactivate (child_compassion/models/project_lifecycle_event.py), which flips exception letters back and resubmits them via gmc_messages.process_messages().

The bug was entirely in the MyCompassion portal (my_compassion), which never passed allow_during_suspension, so it hid the "Write a letter" UI before a letter could ever reach that backend logic:

  • compassion.child._compute_can_write_letter (models/compassion_child.py:91-97) read sponsorship.can_write_letter directly.
  • res.partner._compute_is_writer (models/res_partner.py:155-166) filtered sponsorship_ids on can_write_letter directly.

In v14 this was masked for mobile users because mobile_app_connector/ app_hub.py independently passed allow_during_suspension=True when building the app hub's sponsorship list. That module no longer exists in v18 — the native app (my_compassion_native) is just a thin Capacitor wrapper around the same portal pages — so there was no working reference bypass left anywhere in the v18 codebase; the bug affects web and mobile identically.

What changed

  • models/compassion_child.py: _compute_can_write_letter now reads sponsorship.with_context(allow_during_suspension=True).can_write_letter instead of the plain field, scoped to just this read (does not affect can_i_make_gift, a separate compute).
  • models/res_partner.py: _compute_is_writer now filter sponsorship_ids.with_context(allow_during_suspension=True) before the can_write_letter domain check.

v14 was investigated in parallel (confirmed to have the same underlying gap, masked there by the mobile app's independent bypass) but is explicitly not fixed by this PR — v18 only, per decision at triage as we migrate to v18 soon.

Known separate/open issue found, not fixed

While testing, I found that a letter held as "Exception" is currently invisible to the sponsor: correspondence.is_published (my_compassion/models/correspondence.py:37-39) computes to False whenever an S2B letter's state == "Exception", and both the /my2/children "latest correspondence" widget and the child timeline feed filter strictly on is_published = true.

So today a sponsor who writes a letter during suspension gets no confirmation and the letter disappears from their own timeline entirely until the FCP reactivates. There is also no dedicated email/notification for a letter specifically landing in Exception state (only a generic one-time "Project Suspension" email sent when the FCP first suspends). This same gap also applies to the two other pre-existing hold reasons that reuse "Exception" (sponsorship not yet active, Christmas letters outside the Christmas period).

After talking to the SDS, we will create another ticket to implement this feature to indicate the correspondance journey from the sponsor to the child. We will have to think this through to what to indicate that matters to the enduser as there is 12 state and 3 kind of exceptions for the "Exception" state

How to test manually

  1. Find an active sponsorship whose FCP is Suspended with hold_s2b_letters = true
  2. Log in as admin and switch login to that correspondent (mind that there can be more than one same-named partner in the DB — check res_users.login, not just the name).
  3. On website go to /my2/children/ — the child's "Write a letter" option should now be visible (was hidden before the fix).
  4. Write and submit a letter.
  5. In the backend -> Correspondence, check the new correspondence record: state should be "Exception", with a chatter message "Letter was put on hold because the project is suspended."
  6. Create a Reactivation lifecycle event for the FCP — confirm hold_s2b_letters flips to false and the held letter's state moves out of "Exception" (pre-existing reactivate_letters() flow, unchanged by this fix — confirms the end-to-end path works once the letter can actually be created).

The portal computed can_i_write_letter/is_writer straight off
recurring.contract.can_write_letter, which is False whenever the
project has hold_s2b_letters set, hiding the "Write a letter" option
entirely while the FCP is suspended.

Pass allow_during_suspension in context when computing these, mirroring
what mobile_app_connector already did in v14. The existing
sbc_compassion correspondence.create_commkit()/hold_letters_action()/
reactivate_letters() machinery already holds the letter as an
"Exception" and auto-resubmits it once the FCP reactivates - it was
just never reached because the portal blocked letter creation before
it could run.
@danpa32
danpa32 requested a review from NoeBerdoz July 30, 2026 12:43
Comment on lines +99 to +101
can_write_letter = sponsorship.with_context(
allow_during_suspension=True
).can_write_letter

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Suspension-aware eligibility is inconsistent across portal views

For a terminated sponsorship in its last-letter grace period, a project correspondence hold makes raw sponsorship.can_write_letter false while this contextual lookup makes child.can_i_write_letter true. The timeline consequently offers Write a last letter, but the children controller still groups the sponsorship as ended using raw s.can_write_letter in my_compassion/controllers/my2_children.py:345-355, and the card takes its ended/termination branch from the same raw field in my_compassion/templates/components/my2_children_card.xml:70. Use the suspension-aware eligibility consistently when grouping and rendering this grace-period state so the list/card agrees with the available action.

Artifacts

Focused portal grace-period validation script

  • The executable validation script reads the baseline and changed child compute plus the direct controller and template predicates, then evaluates the stipulated suspended-project grace-period scenario; it establishes the exact inconsistency under test.

Baseline portal grace-period check output

  • Executed baseline check against HEAD^; it shows the uncontextualized compute does not expose the final letter action while raw eligibility still classifies the sponsorship as ended.

Changed portal grace-period check output

  • Executed check against HEAD; it shows the new contextual child eligibility exposes the final letter action while the unchanged raw controller/card predicates still classify the sponsorship as ended, confirming the mismatch.

Odoo runtime availability probe

  • Executed environment probe showing Odoo executables/module/core tree, PostgreSQL, and Docker are absent, so a live portal/database request could not be run.

View artifacts

T-Rex Ran code and verified through T-Rex

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@greptile-apps propose a fix to consistently allow writing within the grace period regardless the suspension status.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the two targeted fixes:

1. my_compassion/controllers/my2_children.py — sponsorship grouping lambdas

The active_sponsorships / ended_sponsorships split uses raw s.can_write_letter, so a grace-period sponsorship on a suspended FCP falls into ended_sponsorships:

Suggested change
can_write_letter = sponsorship.with_context(
allow_during_suspension=True
).can_write_letter
"active_sponsorships": sponsorships.filtered(
# all not terminated or terminated within grace period or exit comm not sent yet
lambda s: s.state != "terminated"
or s.with_context(allow_during_suspension=True).can_write_letter
or not s.exit_communication_sent
),
"ended_sponsorships": sponsorships.filtered(
# terminated and not within grace period, excluding specific end reasons and exit comm sent
lambda s: s.state == "terminated"
and not s.with_context(allow_during_suspension=True).can_write_letter
and s.exit_communication_sent
and s.end_reason_id.name
not in ["Subreject", "Mistake from our staff"]
),

2. my_compassion/templates/components/my2_children_card.xml — card status line

The t-if that switches the card to "Sponsorship terminated on …" also reads raw sponsorship.can_write_letter, which hides the last-contact line even though the action is now available:

Suggested change
can_write_letter = sponsorship.with_context(
allow_during_suspension=True
).can_write_letter
<t
t-if="sponsorship_end_date and not sponsorship.with_context(allow_during_suspension=True).can_write_letter and sponsorship.exit_communication_sent"
>

These two changes mirror exactly what was done in compassion_child.py / res_partner.py and complete the consistent suspension-aware view throughout the portal.

Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danpa32 @Danielgergely Can you review this proposition and see if it makes sense? I have mixed feelings regarding this. Or can we streamline/simplify these cases to avoid recreating the same logic at multiple places?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ecino and @danpa32 I think it might make sense to add a computed field to the recurring.contract model (example: can_write_letter_grace), which then can be read by the controller and the card. (removed duplication and complexity)

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Not safe to merge until the portal list, card, and timeline present a consistent final-letter status for suspended projects.

The affected grace-period flow was reproduced by comparing the previous and changed eligibility behavior. The changed child computation enables the final-letter action, but the unchanged portal list and card continue to use raw eligibility and present the sponsorship as ended.

Files Needing Attention: my_compassion/models/compassion_child.py, my_compassion/controllers/my2_children.py, my_compassion/templates/components/my2_children_card.xml, and my_compassion/templates/pages/my2_child_timeline.xml

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex generated a focused portal grace-period validation script and ran the initial baseline portal grace-period check to establish the starting state for the P2 finding.
  • T-Rex captured the changed portal grace-period check output after updating the check for the latest revision.
  • T-Rex executed the Odoo runtime availability probe to verify the environment accessibility during validation.
  • T-Rex validated contract-level results across revisions: the HEAD^ baseline shows contextual_child_compute=False with the write_last_letter flag false, while HEAD shows contextual_child_compute=True with the write_last_letter flag true, and the runtime probe confirms Odoo components are not present.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P2 Suspended-project grace-period sponsorship is still displayed as ended while its final-letter action is available

    • Bug
      • A terminated sponsorship in its letter-writing grace period, where the project suspension makes raw sponsorship.can_write_letter false but the new context makes child.can_i_write_letter true, is grouped under ended_sponsorships and given the ended/termination card treatment. Opening its timeline exposes the Write a last letter action, producing contradictory portal state.
    • Cause
      • The PR changes only the child-level eligibility compute to call sponsorship.with_context(allow_during_suspension=True).can_write_letter in my_compassion/models/compassion_child.py:99-101. The child-list controller continues to use the uncontextualized s.can_write_letter predicates in my_compassion/controllers/my2_children.py:345-355, and the card template independently uses raw sponsorship.can_write_letter at my_compassion/templates/components/my2_children_card.xml:70. The timeline instead uses the new child field at my_compassion/templates/pages/my2_child_timeline.xml:187-203.
    • Fix
      • Use the same suspension-aware eligibility consistently for the children-page grouping and card status/date condition—for example, derive a shared contextual eligibility value from the sponsorship with allow_during_suspension=True, or consistently consume the child-level can_i_write_letter where the portal is determining the current correspondent’s grace-period state. Preserve the raw terminated state only where an ended badge is intentionally independent of letter eligibility.

    T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "[T3322] FIX allow writing letters while ..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants