# Respond to financial hardship during a repayment chat

Source: https://rasa.community/library/casebook/banking-collections/
Author: Rod Rivera
Published: 2026-10-12T09:00:00.000Z

A customer discussing loan repayments tells the assistant they cannot afford rent. A moment later, they ask for a six-payment plan. How should the assistant respond without losing what the customer just said?

The fictional Northgate Bank example pauses automatic plan selection and preserves the request for support. You will check both the conversation history and the tool that records the plan, including what happens when the support team is unavailable.

> **Customer, authored test input:** No me alcanza para la renta.
>
> **Customer, next authored test input:** Bueno, anóteme en el de seis pagos.

The first line means “I don’t have enough for rent.” The next asks to be put on the six-payment plan. These are supplied text inputs, not transcribed speech. The fictional Northgate Bank tool returns:

```text
{"memory": null, "staging": "hardship_declared"}
{"detail": "hardship_declared", "effects": 0, "reason": "choice_not_confirmed"}
```

## Decide which earlier turns remain relevant

A conversation designer cannot treat every new request as a clean slate. In this teaching policy, difficulty meeting basic needs stops automatic repayment-plan selection. The later plan request supplies intent, but it does not establish that the difficulty was resolved.

The first refusal happens while staging the offer. The proof then inserts staged state directly and attempts the final write. Recording is refused too, with no effect. That second part catches a guard placed only at the start of the conversation.

::::checkpoint{id="banking-collections-decision" question="A customer disclosed hardship, then said yes to a plan. What should this fixture record?" options="The plan because the latest answer wins|No plan; preserve the hardship route|A payment because the plan was selected" answer="1"}

The earlier disclosure remains relevant to the action. A new yes does not establish that hardship was resolved.

::::

## Check whether the final refusal actually matters

The staging refusal alone does not show what the final confirmation check protects. Start instead with a valid staged offer, matching memory tag and completed read-back. Then add the hardship disclosure before recording the plan.

The [three companion controls](https://github.com/RasaHQ/rasa-community-resources/blob/eeb3c73e12f35829857f277ebcb0a169c22a4bd2/examples/mantle-voice-banking-collections-gemini/tests/test_confirmation_boundary.py) compare the normal helper with a deliberately broken copy that removes only its final hardship check. The test changes a function in memory and restores it after the call; it does not edit the installed agent or call a payment service.

| Same current offer and confirmation     | Plan recorded | Recorded effects |
| --------------------------------------- | ------------- | ---------------- |
| No hardship disclosure                  | Yes           | 1                |
| Hardship disclosed; final check present | No            | 0                |
| Hardship disclosed; final check removed | Yes           | 1                |

These are actual local fixture results from 13 October 2026. They isolate the final check: with the other confirmation conditions satisfied, removing it records a plan despite the disclosure. A separate result matters too: refusing the plan does not create a support referral. The assistant still needs to request support and report whether that request was accepted.

From the repository root, fetch the companion update and check out its exact revision before running the controls:

```bash
git fetch origin
git checkout eeb3c73e12f35829857f277ebcb0a169c22a4bd2
python3 -B -m unittest discover \
  -s examples/mantle-voice-banking-collections-gemini/tests \
  -p 'test_confirmation_boundary.py' -v
```

All three controls passed. They exercise the authored Spanish phrases and fixture policy; they do not measure a model’s recognition of hardship.

## Put the hardship stop inside confirmation

The confirmation helper reads the customer history and open referrals before it checks the staged offer and read-back. The hardship stop is the first branch in this excerpt.

[Source excerpt](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-banking-collections-gemini/lib/repayment.py#L556-L565):

```python
if hardship_declared(conversation.user_texts) or state.open_referral():
    return False, "hardship_declared"
staged = state.staged
if offer is None or not staged or staged["offer_id"] != offer["offer_id"] or memory_tag != staged["tag"]:
    return False, "not_staged"
question = fold(conversation.confirmation_question or "")
if fold(_offer_view(offer)["terms_spoken"]) not in question or fold(account["label"]) not in question:
    return False, "not_read_back"
if conversation.confirmation_answer is None:
    return False, "not_answered"
```

This detector reads a bounded set of authored Spanish phrases in the companion source. The checks cover those inputs. They do not establish recall across indirect disclosures, dialects or speech-recognition mistakes. Have the collections owner supply the disclosures that matter for the intended release.

::::callout{type="info" title="Do not turn a pause into a payment"}

The receipt test records a plan choice and explicitly says that no payment was received. Keep agreement, recording a plan and taking a payment separate in both the interface and product metrics.

::::

## Give an unavailable team a different reply

The loan hardship fixture cannot accept a referral. The callback test preserves the request and returns a callback reference. That is a different state from an accepted referral, and neither is a recorded repayment plan.

| Service state                | Permissible next step                         | Claim to avoid                      |
| ---------------------------- | --------------------------------------------- | ----------------------------------- |
| Hardship referral accepted   | Give the referral reference                   | Saying a plan was agreed            |
| Hardship team unavailable    | Preserve request and callback reference       | Saying the team accepted a referral |
| Customer withdraws agreement | Clear staged choice or withdraw recorded plan | Treating an earlier yes as current  |

::::diagram{title="The conversation can leave the offer path before any plan is recorded"}

```dot
history [label="Read customer conversation"]
hardship [label="Hardship disclosed?"]
exit [label="Preserve hardship request or callback"]
offer [label="No hardship: check current offer"]
record [label="Confirm this offer before recording"]
history -> hardship
hardship -> exit
hardship -> offer
offer -> record
```

::::

The support team needs to receive the preserved request. The fixture creates a record; it does not place a phone call. Agree who owns unfulfilled callbacks and what the customer is told when a team remains unavailable.

## Test the final write, not just the first reply

Run these checks from the pinned companion project. They use Python's standard library and make no model calls. The outputs below were recorded on 7 October 2026; elapsed times can differ.

```bash
git clone https://github.com/RasaHQ/rasa-community-resources.git
cd rasa-community-resources
git checkout 4aa0c4419dc193fef7a969c12d59edcf720f2606
cd examples/mantle-voice-banking-collections-gemini/tests
```

::::run{cmd="python3 -m unittest test_guard.SelectAndRecordTests test_guard.WithdrawReferralCallbackTests -q"}
::::

```text
----------------------------------------------------------------------
Ran 19 tests in 0.001s

OK
```

The [companion tests](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-banking-collections-gemini/tests/test_guard.py) contain the assertions for these cases. To print the first-screen trace yourself, run this script from the same directory:

::::solution{title="Show the script that printed the fixture states"}

Paste it into a file such as `trace.py`, then run `python3 trace.py`. It prints selected fields from the actual tool results; it does not invent replies.

```python
"""Run from the pinned companion project tests directory. Stdlib only."""
import json
import test_guard as t

case = t.SelectAndRecordTests()
case.setUp()
users = ("No me alcanza para la renta.", "Bueno, anóteme en el de seis pagos.")
staged, memory = case.stage("OFR-4471-6M", users=users)
print(json.dumps({"staging": staged["status"], "memory": memory}, sort_keys=True))
case.state.staged = {"account_ending": "4471", "offer_id": "OFR-4471-6M", "tag": "OFR-4471-6M/1"}
result = case.record("OFR-4471-6M", {"plan_tag": "OFR-4471-6M/1"}, users=users)
print(json.dumps({"reason": result["reason"], "detail": result["detail"], "effects": result["effects"]}, sort_keys=True))
```

::::

For conversation review, replay the two authored turns before checking a normal yes. Then attempt the final write with staged state and inspect the result. [Design the handoff summary](/library/guides/design-the-handoff-summary/) so it carries the disclosure and unresolved request without inventing a resolved outcome.

::::cta{href="/library/guides/design-the-handoff-summary/" label="Carry the disclosure into the handoff"}
::::

The [complete fixture implementation](https://github.com/RasaHQ/rasa-community-resources/blob/4aa0c4419dc193fef7a969c12d59edcf720f2606/examples/mantle-voice-banking-collections-gemini/lib/repayment.py#L1-L768) defines the service state and remaining branches cited here.