# Nuke a Phone Number for a Clean Signup Restart

## Goal

Give support/engineering one command that removes every signup, text-flow, auth, and payment artifact tied to a phone number so the person can start the RonanRx signup flow as if that phone number never existed.

## Why this is needed

The current all-caps `RESET` text command is a soft Linq conversation reset. It clears the `LinqConversation` shell and detaches the current intake, but it intentionally does not delete the patient graph, onboarding records, auth state, or payment state.

That means a restarted text flow can rediscover the same `Patient` by phone lookup and reuse the old intake/payment state. Payment details can remain stuck because Stripe customer identity is stored on `patients.stripe_customer_id` and subscription state is stored on `patient_program_enrollments.stripe_subscription_id` / `subscription_status`.

This item is for an operator-run nuke command, not for changing inbound SMS behavior by default.

## Suggested command shape

```sh
bin/rails "support:nuke_phone[+15555550123]"
bin/rails "support:nuke_phone[+15555550123,dry_run]"
bin/rails "support:nuke_phone[+15555550123,force]"
```

Default mode must be dry-run. `force` is the only mode that deletes or calls Stripe.

## Implementation target

Add:

- `app/services/support/phone_number_nuke.rb`
- `lib/tasks/support.rake`
- `test/services/support/phone_number_nuke_test.rb`
- `test/lib/tasks/support_nuke_phone_test.rb`

Follow the existing rake task style in `lib/tasks/synthetic_data.rake` and `lib/tasks/completion_demo.rake`.

## Core lookup inputs

The service should normalize the phone once, then derive every system-specific lookup key:

- E.164 phone via the existing phone normalization helper (`PhoneBinding.normalize` if present in the target branch).
- Patient phone hash via `Patient.hash_phone(normalized_phone)`.
- Phone binding digest via `PhoneBinding.digest(normalized_phone)`.
- Public intake phone hash via `PublicIntake.hash_phone(normalized_phone)`.
- Linq contact identifiers needed to find conversations for that sender. Reuse the same contact-key/chat-key derivation used by `Api::V1::LinqController`.

Fail closed if the phone cannot be normalized.

## Current code paths to inspect

- SMS reset: `app/controllers/api/v1/linq_controller.rb`, `app/services/linq/conversation_reset.rb`, `app/services/linq/capture.rb`.
- Signup reuse by phone: `app/controllers/api/v1/signups_controller.rb` and/or `app/services/linq/signup_forward.rb`.
- Patient lookup/payment identity: `app/models/patient.rb`, `app/models/patient_program_enrollment.rb`, `app/services/onboarding/prepare_payment.rb`, `app/services/onboarding/activate_subscription.rb`.
- Existing payment cancellation helper: `app/services/onboarding/cancel_subscription.rb`.
- Existing wipe task patterns: `lib/tasks/synthetic_data.rake`, `lib/tasks/completion_demo.rake`.

## Dry-run summary

Dry-run should print counts and categories only. It must not print the raw phone number, patient names, emails, addresses, tokens, OTPs, Stripe IDs, or message bodies.

Include counts for:

- Matching `Patient` rows.
- Matching patient `User` rows and orphaned patient users that would be deleted.
- Matching `PatientProgramEnrollment` rows.
- Matching `IntakeResponse` rows.
- Matching `SecureLink` rows.
- Matching `LinqConversation`, `LinqEvent`, and `LinqMediaAttachment` rows.
- Matching `PublicIntake` rows.
- Matching phone-bound auth/session rows: `LoginChallenge`, `LoginAudit`, `ConnectSessionGrant`, `Consent`, `AuthorizationToken`, and related access-log rows where applicable.
- Matching rate-limit rows keyed by phone digest, if the target branch stores phone throttles there.
- Stripe subscriptions that would be canceled and Stripe customers/payment methods that would be detached or deleted.

## Destructive behavior

In `force` mode, do the work in a dependency-safe order:

1. Normalize the phone and take the same per-phone advisory lock used by signup, so a new signup cannot race the nuke.
2. Find matching patients by `Patient.hash_phone`.
3. Find matching phone-bound users by `PhoneBinding.digest`.
4. Find matching public intakes by `PublicIntake.hash_phone`.
5. Find matching Linq conversations by the same sender/contact identifiers used by inbound Linq.
6. Cancel any real Stripe subscriptions for matching enrollments using `Onboarding::CancelSubscription` or equivalent existing service.
7. Detach/delete remote Stripe customer/payment-method state for matching patients, then clear local Stripe IDs. If the target branch cannot safely delete the remote customer, detach saved payment methods and delete local customer references.
8. Detach `LinqConversation.intake_response_id` before deleting intakes, or destroy the conversations first.
9. Delete `LinqMediaAttachment` and `LinqEvent` rows for matching conversations.
10. Delete matching `SecureLink` rows before deleting `IntakeResponse` rows.
11. Null any intake `consent_id` / `id_document_id` references that would otherwise block deletion.
12. Delete phone-bound auth/session rows and append-only log rows using the same direct-delete pattern already used by existing wipe tasks when model callbacks intentionally block normal destroy.
13. Destroy matching `Patient` rows and rely on existing `dependent: :destroy` associations for the patient graph.
14. Destroy orphaned chat-created patient `User` rows after patient deletion. Do not delete staff users or users with unrelated records.
15. Delete matching `PublicIntake` rows if the product decision is "entire system" rather than only Linq signup.
16. Return a final category/count summary and a success/failure status.

The command should be idempotent: running it twice should succeed, with the second run reporting zero remaining local records.

## Acceptance criteria

- `bin/rails "support:nuke_phone[PHONE]"` performs a dry-run and changes nothing.
- `bin/rails "support:nuke_phone[PHONE,force]"` removes the phone from local signup, Linq, auth/session, public-intake, and payment state according to the chosen scope.
- A nuked phone can text in and receive a brand-new signup/intake/payment flow immediately.
- Existing all-caps SMS `RESET` behavior remains soft unless the product decision explicitly changes it.
- The command cancels or detaches remote Stripe state before local payment references disappear.
- The command logs who/what/when at category-count level without printing sensitive values.
- The service has focused tests for dry-run, invalid phone, full deletion, Stripe cancellation, multiple patients sharing a phone, unrelated records, and idempotent second run.
- The rake task has a task-level test that verifies dry-run default and `force` dispatch.

## Open product decisions

- Should all-caps SMS `RESET` stay soft-only, with destructive nuke available only through the support command? Recommended: yes.
- Should the nuke include `PublicIntake`, `LoginAudit`, rate-limit rows, and Linq message history, or only Linq signup/onboarding/payment state? Recommended for this item: include them so "entire system" is true.
- Should remote Stripe customer deletion be attempted, or should the implementation only cancel subscriptions, detach payment methods, and clear local references? Recommended: cancel subscriptions and detach payment methods first; delete the remote customer only if Stripe allows it cleanly in the active mode.
