Skip to content

fix: notify returning Play subscribers about renewal - #776

Merged
thomasluizon merged 3 commits into
mainfrom
thomasluizon/ticket-1548-play-renewal-reactivation
Oct 19, 2026
Merged

thomasluizon merged 3 commits into
mainfrom
thomasluizon/ticket-1548-play-renewal-reactivation

Conversation

@thomasluizon

@thomasluizon thomasluizon commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

A Play subscriber who reactivates after requesting account deletion now receives one unread notice explaining that renewal is off and how to restart it in Google Play. The same transaction clears deletion ownership and removes the matching owed Play cancellation, so a delayed cancellation retry cannot undo the person's recovery action.

Closes thomasluizon/orbit-tickets#1548

Approach

  • Auth/Commands/AccountReactivation.cs centralizes the ownership check and localized notification creation. Both VerifyCodeCommand and GoogleSignInFlow call it inside their existing sign-in transaction, using the stored account language. The existing notification query exposes the notice to web and Android.
  • User.CancelDeactivation clears PlayRenewalStoppedByDeletion while retaining Stripe ownership for its existing restoration job. The existing paid-period entitlement is preserved.
  • The API dependency registration and existing constructor-based tests receive the shared reactivation service. No endpoint, DTO, database schema, validation input, or minimum app version changes. Existing sign-in validators and domain guards remain the input boundary. OpenAPI generation produced no contract diff.
  • PlayReactivationTests drives the real deletion cancellation job, both real sign-in paths, relational persistence, the owed cancellation retry, and the shared notification query. Provider ports are mocked; SQLite uses the repository's existing unit-test factory. New reactivation code makes no provider calls.

Google API evidence

Checked the live reference pages on 2026-10-19:

  • purchases.subscriptionsv2, page last updated 2026-09-28 UTC: lists cancel, defer, get, and revoke; no restart or resume method. A canceled, unexpired auto-renewing purchase is SUBSCRIPTION_STATE_CANCELED with renewal disabled.
  • purchases.subscriptions, page last updated 2026-09-28 UTC: lists acknowledge and deprecated cancel/defer; no restart method.
  • subscriptionsv2.cancel, page last updated 2025-09-11 UTC: USER_REQUESTED_STOP_RENEWALS allows restoration by the subscriber. This is the cancellation type already used by Orbit. DEVELOPER_REQUESTED_STOP_PAYMENTS cannot be restored.
  • Subscription lifecycle: resubscribe, page last updated 2026-10-15 UTC: the subscriber uses Resubscribe to restore a canceled, unexpired subscription; an expired purchase may require subscribing again. This supports the notice path rather than a provider restoration call.
  • Google Play Help confirms Resubscribe under subscriptions and notes that the option may be unavailable. This page shows no update date.

No new external API method or response field is read by this change. No invented provider response fixture is needed; the tests use Orbit's existing IPlayBillingService port and domain factories.

Assumptions

  • Create the notice synchronously inside the existing reactivation transaction; rejected a background notice because reactivation, ownership cleanup, and the notice must commit together.
  • Notify flagged accounts even after the paid period expires, with a purchase fallback; rejected silently dropping the notice for expired subscriptions.
  • Remove only an owed Google Play cancellation whose identifier matches the deletion-owned purchase token; rejected retaining that retry because it could cancel renewal after recovery. Unrelated tokens and Stripe records remain.
  • Leave the notification URL unset and give Google Play steps in its body; rejected an external notification URL because the notification contract accepts only app-relative paths.
  • Reused the existing notification inbox and dedupe index; rejected introducing a new contract or migration.
  • An unknown purchase does not confirm renewal is off, so it produces no notice.

Test evidence

Regression: VerifyCodeCommandHandlerTests.Reactivation_RestoresOnlyUnexpiredStripeRenewal and GoogleCodeAuthCommandHandlerTests.Reactivation_RestoresOnlyUnexpiredStripeRenewal.

  1. With the defect present and the existing tests unchanged, dotnet test tests/Orbit.Application.Tests --filter 'FullyQualifiedName~Reactivation_RestoresOnlyUnexpiredStripeRenewal' --verbosity quiet passed all 8 cases, exit 0.
  2. Before changing production code, strengthened those tests to record deletion-owned Play cancellation and assert the ownership flag clears. The exact same command failed 4 Play cases and passed 4 Stripe cases, exit 1. The failing assertion was the Play ownership flag remaining true after sign-in.
  3. After the implementation, added a notice assertion to those same tests. The same command passed all 8 cases, exit 0.

Additional focused coverage includes en and pt-BR, both sign-in methods, paid and expired subscriptions, duplicate sign-ins, independently canceled subscriptions, matching versus unrelated owed records, delayed cancellation retries, and save/commit rollback followed by a successful retry. dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~PlayReactivationTests|FullyQualifiedName~DeletionCancellationPersistenceTests' --verbosity quiet passed all 33 cases, exit 0. dotnet test tests/Orbit.Domain.Tests --filter 'FullyQualifiedName~UserDeletionCancellationTests' --verbosity quiet passed all 22 cases, exit 0.

Final verification, all exit 0:

  • dotnet build Orbit.slnx --verbosity quiet: 0 errors; existing dependency, versioning, and obsolete-API warnings only. OpenAPI output is unchanged.
  • dotnet test tests/Orbit.Infrastructure.Tests --filter 'FullyQualifiedName~PlayReactivationTests' --verbosity quiet: 14 passed.
  • dotnet test tests/Orbit.Domain.Tests --verbosity quiet: 750 passed.
  • dotnet test tests/Orbit.Application.Tests --verbosity quiet: 5,573 passed.
  • dotnet test tests/Orbit.Infrastructure.Tests --verbosity quiet: 3,801 passed.
  • Git hooks passed formatting, dash, timelessness, root-file, and suppression checks. No new bare line comments. No server or browser was started. Mutation testing and CI waiting are left to the orchestrator.
  • Unchanged baseline: dotnet test tests/Orbit.Infrastructure.Tests --filter FullyQualifiedName~PlayReactivationTests --verbosity minimal: 14 passed with the defect present.
  • Red: dotnet test tests/Orbit.Infrastructure.Tests --no-restore --filter FullyQualifiedName~SignIn_DeletionOwnedButPlayStillRenewing_DoesNotNotify --verbosity minimal: 2 failed, both finding one notice while Play still renewed.
  • Green on committed code: dotnet test tests/Orbit.Infrastructure.Tests --no-build --no-restore --filter FullyQualifiedName~PlayReactivationTests --verbosity minimal: 25 passed, including both regression cases through the queued job.
  • Focused auth tests: 120 passed.
  • dotnet build Orbit.slnx --no-restore --verbosity minimal: 0 errors. Generated OpenAPI files unchanged.
  • Repository commit hooks passed.

Full projects, each run with dotnet test <path> --no-build --no-restore --verbosity minimal:

Project path Passed Failed Skipped Exit
tests/Orbit.Domain.Tests 750 0 0 0
tests/Orbit.Application.Tests 5,573 0 0 0
tests/Orbit.Infrastructure.Tests 3,812 0 0 0

Copy

English title: Your Google Play renewal is off

English body: Your Orbit account is active again. Deleting your account turned off your Google Play renewal. To renew again, open Google Play, go to Payments & subscriptions > Subscriptions, select Orbit, and tap Resubscribe. If that option is unavailable, subscribe to Pro again in Orbit.

pt-BR title: Sua renovação no Google Play está desativada

pt-BR body: Sua conta do Orbit está ativa de novo. A renovação no Google Play foi desativada quando você pediu a exclusão da conta. Para voltar a renovar, abra o Google Play, vá em Pagamentos e assinaturas > Assinaturas, escolha Orbit e toque em Assinar novamente. Se essa opção não aparecer, assine o Pro de novo pelo Orbit.

Reviewed both languages through the repository's /second-opinion helper with gpt-5.6-sol. The final review rejected the candidate finding that expired subscribers had no recovery path: both strings offer purchasing Pro again through Orbit, and the pt-BR wording was judged clear and natural. The copy is included for the orchestrator's approval.

Manual steps

None beyond normal API deployment. No new configuration, secrets, console settings, migrations, or backfills are required.

@thomasluizon

Copy link
Copy Markdown
Owner Author

Copy approval (/second-opinion, claude-opus-5-5, framed as a claimed copy defect, so DISAGREE approves): DISAGREE, medium confidence. "The pt-BR body is idiomatic rather than word-for-word: it restructures the cause ('foi desativada quando você pediu a exclusão da conta') and uses Brazilian phrasing like 'vá em', 'de novo' and 'pelo Orbit'. The menu path and button names ('Payments & subscriptions > Subscriptions', 'Resubscribe' / 'Pagamentos e assinaturas > Assinaturas', 'Assinar novamente') match Google Play's labels, keep Orbit/Pro untranslated, and clearly state what happened and what to do, so no concrete defect is shown." The en and pt-BR notice strings are approved as written.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Important

The new notice can tell a returning subscriber that renewal is off while Google Play will still renew and charge. Cancellation ownership is an intent marker, not confirmation that the provider accepted cancellation.

Reviewed changes: Reviewed the account-reactivation notification, billing ownership cleanup, both authentication paths, and their regression coverage.

  • Shared reactivation flow: Email-code and Google sign-in invoke the new scoped AccountReactivation service within their existing transactions.
  • Play recovery notice: Deletion-owned Play subscriptions receive localized renewal instructions, and matching owed cancellation records are removed.
  • Ownership and persistence: Reactivation clears Play ownership while retaining Stripe restoration behavior, with SQLite tests for notification persistence, entitlement preservation, retries, and rollback.
  • Verification: All 63 focused tests passed; a separate temporary reproduction confirmed the failure-before-provider-mutation case described inline.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread src/Orbit.Application/Auth/Commands/AccountReactivation.cs Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes: Reviewed 0fe80701 since the prior Pullfrog review at 98107fd5, with the full PR diff as context.

  • Verified renewal before notification: Moved notice creation into a post-sign-in job that checks Play outside the database transaction and creates a notice only when renewal is confirmed off.
  • Preserved safe delivery: Added account and purchase-token rechecks under the user row lock, provider-failure retries, and per-reactivation deduplication that also respects dismissed notices.
  • Expanded regression coverage: Added failure-before-cancellation, unknown-purchase, account-change, rollback, and replay cases. All 131 focused Infrastructure, Application, and Domain tests passed.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit 96023f9 into main Oct 19, 2026
25 checks passed
@thomasluizon
thomasluizon deleted the thomasluizon/ticket-1548-play-renewal-reactivation branch October 19, 2026 11:09
thomasluizon added a commit that referenced this pull request Oct 19, 2026
…d cancellations and the Play renewal notice (#746) (#778)

* Deprecate AI memory HTTP contracts for ORB-229 (part 1) (#771)

(cherry picked from commit f8abd02)

* Deprecate challenge and accountability routes for ORB-212 (#772)

(cherry picked from commit 572b8de)

* Deprecate social API routes and required profile fields (ORB-233 part 1) (#773)

(cherry picked from commit 9c414c7)

* fix: exclude retired notifications from inbox counts (#774)

(cherry picked from commit f5df366)

* fix: retain owed cancellations after account erasure (#775)

* fix: retain provider cancellation obligations after account erasure

* fix: preserve erasure for accounts without billing obligations

* fix: claim owed cancellations before provider calls

* fix: alert owed cancellation exhaustion only after a failed attempt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c369535)

* fix: notify returning Play subscribers about renewal (#776)

* fix: explain Play renewal recovery on account reactivation

* test: register reactivation service in sign-in retry fixture

* fix: verify Play renewal before reactivation notice

(cherry picked from commit 96023f9)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant