Skip to main content

ManualSubscriberController

\Benecaster\REST\ManualSubscriberController

Class Premium

Admin endpoint that comps a subscriber onto a paid tier without going through Stripe — the “Add subscriber manually” action. Registers POST /wp-json/benecaster/v1/shows/{id}/subscribers/manual with permission_callback_admin (manage_options + wp_rest nonce).

Flow: validate the show and that tier_slug maps to an active, paid tier (free/follower tiers are rejected) → gate on benecaster_bridge_supports_manual_membership for the show’s active bridge, 501 bridge_unsupported when the bridge doesn’t advertise support (only NativeBridge does in Benecaster; bridge add-ons flip the filter when they ship a real implementation) → validate optional expiry_at and joined_at before anything is created, so a bad date 400s with nothing left to clean up → resolve or create the WordPress user by email → reject with 409 if the user already has an active subscription for the show → delegate provisioning to the active bridge’s create_manual_membership() → optionally backdate the newly-minted token’s created_at to joined_at so TenureBadgeCalculator credits tenure earned on another platform, rather than stamping the supporter as brand new.

Constructor Dependencies

Type Description
\Benecaster\Membership\MembershipTierRepository Resolves and validates the requested tier slug against the show's active, paid tiers.
\Benecaster\Membership\SubscriptionRepository Checks for an existing active subscription before provisioning.
\Benecaster\Bridge\BridgeManager Resolves the show's active bridge to check manual-membership support and delegate provisioning.
\Benecaster\Token\TokenRepository Looks up the newly-minted token row to backdate created_at when joined_at is supplied.

Methods

Method Visibility Since Description
register_routes(): void Public — Registers POST /shows/{id}/subscribers/manual with permission_callback_admin and a numeric-id validate_callback.
create( WP_REST_Request $request ): WP_REST_Response|WP_Error Public — Handles POST /shows/{id}/subscribers/manual. Body: email (required), tier_slug (required, must be an active paid tier), display_name (optional, applied only when a NEW user is created — an existing user's display name is left alone), expiry_at (optional ISO 8601, parsed to UTC by parse_expiry() — an hourly job revokes the token past this time), joined_at (optional Y-m-d or Y-m-d H:i:s UTC, validated by BackdateGuard). 400s on invalid email, missing/invalid/free tier_slug, invalid expiry_at, or a joined_at BackdateGuard rejects (future date, or earlier than BackdateGuard::FLOOR). 501 bridge_unsupported when the active bridge doesn't support manual membership — both from the pre-check filter and, as defence in depth, when create_manual_membership() itself throws BadMethodCallException. 409 already_subscribed when the resolved user already holds an active subscription for the show. 500 token_generation_failed if the bridge creates the subscription but no token is minted. On success (201): resolves/creates the WP user via resolve_user_id(), delegates to the bridge's create_manual_membership(), backdates the token's created_at when joined_at was given, fires benecaster_manual_subscriber_created, and returns {subscription_id, token_hash_prefix, user_id}.
resolve_user_id( string $email, string $display_name, int $show_id = 0 ): int|WP_Error — — Returns an existing WP user's ID by email, or creates one with wp_create_user() and a random 24-character password. On creation: marks the account via GeneratedPasswordMarker::mark() (nobody ever learns this password, so the account page's current-password gate must not apply to it), fires benecaster_subscriber_account_provisioned with source 'manual_add' (which triggers Benecaster's guest_password_set email so the person can get into the account made for them), and applies display_name if non-empty. Fires the provisioning action ONLY on creation — an account that already existed is left untouched, so the operator adding an existing user is never mistaken for triggering a password-reset email at them.
derive_username( string $email ): string — — Derives a WP login from the email's local part via sanitize_user(), falling back to 'subscriber' if that sanitizes to empty, and appending a numeric suffix (-2, -3, …) until the login is free.
parse_expiry( string $raw ): ?string — — Parses an ISO 8601 string to a UTC 'Y-m-d H:i:s' MySQL DATETIME string via DateTimeImmutable, returning null on a malformed value so the caller can 400. The hourly expiry job compares against a PHP-computed gmdate() cutoff bound into its query, not MySQL's clock.
joined_at_message( string $reason ): string — — Translates a BackdateGuard reason code (REASON_FUTURE, REASON_BEFORE_FLOOR, or unrecognized) into an operator-facing message, distinct from the guard's own developer-oriented exception text.

Hooks Fired

Notes

Extends the plugin's internal RestController base — not part of the public API; its own docblock tells add-on developers not to extend it, and to register independent routes on rest_api_init with the public benecaster_rest_permission_admin() function instead. Inherits register() (hooks register_routes() onto rest_api_init), the permission_callback_admin / permission_callback_editor / permission_callback_subscriber callbacks, rest_error(), and not_found().

benecaster_subscriber_account_provisioned fires with source: 'manual_add' from this class; the same hook fires with other source values from other provisioning flows (bulk enrollment, email-first signup, the donation webhook) — see the hook's own CPT entry for the full list.