# Membership Auto-Renew

## Scope and safety boundary

Membership Auto-Renew is a TakafulHub Membership-only card facility. It is not
available to an approved, active VastPro account and never creates a Membership
requirement for VastPro. The initial card checkout creates a CHIP Client, shows
only the recurring-capable card methods that CHIP exposes, and records explicit
consent only after the normal Membership payment is verified and fulfilled.

No raw card number, CVV, expiry date, or CHIP credential is exposed in a UI,
log, notification, payment-attempt metadata, or API response. The CHIP client
identifier and recurring token use Laravel encrypted casts; billing receives
only the provider's safe card description.

## Renewal cycle

The source of truth for the due time is the active Membership's `ends_at` in
`Asia/Kuala_Lumpur`. The hourly `memberships:process-auto-renewals` scheduler
uses a stable key made from Membership id plus that exact due timestamp. The
database has a unique key for that period, so a cycle has one aggregate and one
MembershipOrder (`origin=auto_renew`), while retry attempts create additional
PaymentAttempts for that same order.

Before creating a provider Purchase the processor locks the Membership, User,
payment profile and cycle record and re-checks applicability, expiry, current
membership identity, active Auto-Renew consent and a usable CHIP credential.
The provider call happens only after that reservation transaction commits.

The processor creates and stores a new CHIP Purchase reference first, then
calls the documented `POST /purchases/{id}/charge/` endpoint using the saved
`recurring_token`. A `pending_charge` result remains `awaiting_callback`; only
the existing signed CHIP callback retrieves and verifies the Purchase, marks
the payment paid and fulfils the Membership. No scheduler response is itself a
fulfilment signal.

Temporary synchronous failures use at most two scheduled retries (three
charges total). Invalid recurring tokens disable Auto-Renew and do not retry.
A lost transport response after the charge is classified as
`reconciliation_required`, so the system never blindly charges again. This is
the intentional residual crash/transport safety boundary: a staff/provider
reconciliation is required before another irreversible provider action.

## Notifications and reminders

`membership_auto_renewed` and `membership_auto_renew_failed` are required
in-app and email notifications and are deduped by the renewal aggregate. They
replace generic Membership renewal/payment notifications for `auto_renew`
orders. Existing expiry reminders continue to be sent by the Membership
notification scheduler and can use the current Auto-Renew state in their copy.

## Production activation gate

The engine defaults to off through `MEMBERSHIP_AUTO_RENEW_ENABLED=false`.
Enable it in production only after: CHIP recurring-charge merchant approval;
production callback signature verification; HTTPS callback reachability;
`php artisan schedule:run` every minute; queue/worker monitoring where the
deployment uses queues; alerting for failed and reconciliation-required cycles;
and a controlled test-card renewal/fulfilment/retry run. There is no Admin
"Charge now" control, grace period, second-card facility, coupon automation or
non-CHIP recurring provider in this release.

## Card recovery and replacement (Phase 3)

There is one authoritative Membership card. Billing never collects card data:
**Update Card** opens a CHIP-hosted card pre-authorisation checkout using the
existing CHIP Client, `skip_capture=true`, a zero-total documented setup
Purchase, and recurring-card capability. It does not create or fulfil a
MembershipOrder and never changes `ends_at`.

The signed callback accepts only a verified `preauthorized` recurring-token
setup. It confirms the new token with CHIP, atomically makes it authoritative,
then attempts to revoke the superseded token. The old card is never revoked
before the new credential is usable. If that later revoke fails, the new card
remains authoritative and only an encrypted superseded reference is retained
for provider reconciliation. A replayed callback cannot swap twice.

An explicit Auto-Renew OFF persists through card replacement and later manual
Membership renewal; it may only be turned on by the user from Billing. Initial
first qualifying Membership checkout still defaults it on. Card update and
removal are blocked while the current due-cycle provider charge is in flight.

**Remove Saved Card** first disables Auto-Renew, then requests CHIP token
deletion. On success the profile is retained as audit history with token state
`revoked`; the Membership remains unchanged. On CHIP revoke failure the local
state is `revoke_pending`, Auto-Renew is off, and the encrypted credential is
retained solely for safe operational revocation retry—TakafulHub will not use
it for another automatic charge.

Billing distinguishes ON/OFF, Processing, Retry Scheduled, Payment Method
Required and terminal failure. A retry after expiry never promises continuous
access; a later verified success follows the existing MembershipService
reactivation/renewal behaviour. Failure and expiry recovery calls-to-action
lead to Billing; users can always use the normal manual Membership checkout.

### Provider UAT and activation

Before enabling the feature gate, verify in CHIP sandbox: initial card setup,
token creation, OFF/ON, update card, remove card, due charge, callback,
successful renewal, temporary failure/retry, invalid token, and no duplicate
charge. Then use one controlled internal production account before setting
`MEMBERSHIP_AUTO_RENEW_ENABLED=true`. Confirm the merchant has recurring
card-on-file, token-revoke and callback capability; configure Laravel scheduler
and operational monitoring first. Do not automatically migrate or charge
existing production profiles without credential validity review.
