Version

Retention

Revenue Recovery's cancellation screen as a connector's member surface runs it — the question and the one offer in the core's words, the reason and the note written down, the offer taken or passed on, and every refusal with a stable reason.

Subscriby\Connector\Core\Retention. The screen is the core's: the creator decides whether the question is asked and which offers the project makes, the core picks the one offer the member's payment method can honour and phrases every sentence in the member's language, and the dashboard reads every answer. A connector is one more place a member meets it. The contract takes the InstallationRef the conversation runs on and the IdentityRecord of the account that is talking, finds the project behind the one and the member behind the other, and answers in summaries whose every sentence the core already wrote. A bot never composes an offer from a kind and a figure; it prints what it was handed and reports what was tapped, and the portal and the bot ask and offer the same things in the same words.

cancellationScreenFor()

$screen = $this->retention->cancellationScreenFor($bot->installationRef(), $chat->identityRecord());

if ($screen === null) {
    return;                                   // no live membership to cancel: no screen
}

$screen->subscriptionId;                      // the membership the screens are about
$screen->projectName; $screen->planName;      // for the confirmation's words
$screen->asksReason;                          // false when the creator switched the question off
foreach ($screen->reasons as $reason) {
    $reason->code;                            // too_expensive
    $reason->label;                           // "Too expensive", in the member's language
}
$screen->offer?->kind;                        // RetentionOfferKind::Pause | Discount | ReturnCoupon, or null for no offer
$screen->offer?->headline;                    // "Take a month off instead?"
$screen->offer?->sentence;                    // what the offer does, in one sentence
$screen->offer?->acceptLabel;                 // the button's words
$screen->offer?->couponCode;                  // the code, on a return coupon
$screen->accessEndsOn;                        // when a cancellation would end access, formatted, or null

Null while the account holds no live membership to cancel. reasons is empty when the project switched the question off, and offer is null when the project makes no offer on the member's payment method or the creator's plan lacks the offers, so a connector draws the screens it is handed and no other: the reason when asksReason, the offer when there is one, then its own confirmation.

recordCancellationFeedback()

try {
    $record = $this->retention->recordCancellationFeedback($installation, $chat->identityRecord(), $code, $note);
} catch (RetentionRefused $refused) {
    $this->send($chat, $refused->getMessage());   // reason_unknown, invalid_note, ...
}

$record->id;                                  // park it for the offer step
$record->offer;                               // the offer to show next, derived again by the core, or null

Writes the row the dashboard's Cancellations face shows, tells the creator under the Revenue Recovery alert class, raises subscription.cancellation_feedback, and answers the offer to show next. $note is the sentence the member typed or null when they skipped it; a note over 500 characters is invalid_note, a code the screen did not list is reason_unknown.

takeOffer()

try {
    $outcome = $this->retention->takeOffer($installation, $chat->identityRecord(), $record?->id);
} catch (RetentionRefused $refused) {
    // cancellation_unknown, offer_unavailable, gateway_refused
}

$outcome->kind;                               // what they got
$outcome->keepsSubscription;                  // true for a pause or a discount, false for a return coupon
$outcome->sentence;                           // what happens next, in the member's language
$outcome->couponCode;                         // the code to restate, on a return coupon

The offer is derived again by the core, never taken from the connector, so a stale screen cannot claim an offer the creator has since withdrawn (offer_unavailable). A pause holds the charge at the gateway first and pauses access second; a discount is applied at the gateway; a return coupon is handed over on the screen and in a message, and the cancellation goes on, which is why keepsSubscription is what a connector branches on: a pause or a discount ends the conversation with the sentence and a way back, a return coupon goes on to the connector's own confirmation with the code restated. gateway_refused means the gateway would not hold the charge or carry the discount; relay the sentence and offer the confirmation. Pass null as the id when the project asked no question.

declineOffer()

$record = $this->retention->declineOffer($installation, $chat->identityRecord(), $record?->id);   // null when no offer was made

Notes that the member passed on the offer, so the offers shown are counted against the offers taken on the creator's figures. Call it when the member taps past the offer, before the confirmation.

Refusals

Every method throws Subscriby\Connector\Exceptions\RetentionRefused. getMessage() is the core's sentence in the member's language, which is what you relay; reason is stable and yours to branch on: installation_unknown (the ref names no project), member_unknown, subscription_unknown (no live membership), reason_unknown, invalid_note, cancellation_unknown (the id names none of the member's rows), offer_unavailable and gateway_refused.

What is not here

The cancellation itself stays the connector's own confirmation, run through the subscription port as it always was, so a free trial still cancels outright and the decoy-guarded confirmation keeps its code. The creator's switches are RetentionManagement's; the grace ladder, the sequences and the figures are the core's whole subsystem and never a connector's to write.

Read it fresh

The screen and the offer are computed when you ask; park the record's id across two taps, never the screen. The creator may switch the question or an offer off between them, and the core answers offer_unavailable rather than honouring a screen that no longer stands.

How is this guide?

Last updated on

Version

On this page

Subscriby is a product
designed by you — for you.
No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request

For AI agents: llms.txt · llms-full.txt