Created
September 1, 2025 23:04
-
-
Save ara303/b8b9e308accc4939368bb23c421b368f to your computer and use it in GitHub Desktop.
dogshit gpt5 subscription system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Plugin Name: Woo Subscriptions: Shared Access + Renewal Notices | |
| * Description: Extend WooCommerce Subscriptions so multiple users share one subscription’s benefits; includes admin UI to assign members and a renewal notice hook framework. | |
| * Author: GPT | |
| * Version: 1.0.0 | |
| * Requires at least: 6.0 | |
| * Requires PHP: 7.4 | |
| * License: GPLv2 or later | |
| * | |
| * Assumptions: | |
| * - WooCommerce and WooCommerce Subscriptions are active. | |
| * - Relationship storage: subscription post meta `_wcss_shared_members` holds an array of user IDs. | |
| * - “Owner” = the subscription’s customer user. “Members” = additional linked users. The UI avoids the words “Parent/Child.” | |
| */ | |
| if( ! defined( 'ABSPATH' ) ){ exit; } | |
| if( ! class_exists( 'WCSS_Shared_Subscriptions' ) ){ | |
| final class WCSS_Shared_Subscriptions { | |
| const META_KEY_MEMBERS = '_wcss_shared_members'; | |
| const CRON_HOOK_BEFORE = 'wcss_subscription_renewal_notice_before'; | |
| const CRON_HOOK_AFTER = 'wcss_subscription_renewal_notice_after'; | |
| const ACTION_NOTICE = 'wcss_subscription_renewal_notice'; // ($subscription_id, $when) where $when = 'before'|'after' | |
| const NONCE_ACTION = 'wcss_assign_members'; | |
| const CAPABILITY = 'manage_options'; | |
| private static $instance = null; | |
| public static function instance(){ | |
| if( self::$instance === null ){ | |
| self::$instance = new self(); | |
| } | |
| return self::$instance; | |
| } | |
| private function __construct(){ | |
| add_action( 'plugins_loaded', array( $this, 'maybe_boot' ), 5 ); | |
| } | |
| public function maybe_boot(){ | |
| if( ! function_exists( 'wcs_get_subscription' ) ){ | |
| add_action( 'admin_notices', array( $this, 'admin_notice_missing_wcs' ) ); | |
| return; | |
| } | |
| // Public API: helper functions | |
| require_once __FILE__; // self-contained single file; helpers defined at bottom guarded by function_exists. | |
| // Admin UI | |
| add_action( 'admin_menu', array( $this, 'register_admin_ui' ) ); | |
| add_action( 'admin_post_wcss_save_members', array( $this, 'handle_save_members' ) ); | |
| // Ensure members can "benefit" in common checks | |
| add_filter( 'woocommerce_can_subscription_be_updated_to', array( $this, 'allow_members_to_view_update' ), 10, 3 ); | |
| add_filter( 'woocommerce_current_user_can_view_subscription', array( $this, 'extend_view_caps' ), 10, 2 ); | |
| // Schedule notices on relevant changes | |
| add_action( 'woocommerce_subscription_status_updated', array( $this, 'reschedule_notices_on_status_change' ), 10, 3 ); | |
| add_action( 'woocommerce_subscription_date_updated', array( $this, 'reschedule_notices_on_date_change' ), 10, 3 ); | |
| add_action( 'woocommerce_scheduled_subscription_payment', array( $this, 'reschedule_notices_generic' ), 10, 1 ); | |
| // Cron handlers -> unify through a single action for reusability | |
| add_action( self::CRON_HOOK_BEFORE, array( $this, 'emit_notice_before' ), 10, 1 ); | |
| add_action( self::CRON_HOOK_AFTER, array( $this, 'emit_notice_after' ), 10, 1 ); | |
| // Example listener wiring (no-op email framework; developers can hook their own) | |
| add_action( self::ACTION_NOTICE, array( $this, 'default_email_listener' ), 10, 2 ); | |
| // Activation/Deactivation | |
| register_activation_hook( __FILE__, array( __CLASS__, 'activate' ) ); | |
| register_deactivation_hook( __FILE__, array( __CLASS__, 'deactivate' ) ); | |
| } | |
| public function admin_notice_missing_wcs(){ | |
| if( current_user_can( self::CAPABILITY ) ){ | |
| echo '<div class="notice notice-error"><p><strong>Woo Subscriptions: Shared Access + Renewal Notices</strong> requires WooCommerce Subscriptions. Activate it.</p></div>'; | |
| } | |
| } | |
| /* ========================= | |
| * Admin UI | |
| * ========================= */ | |
| public function register_admin_ui(){ | |
| add_submenu_page( | |
| 'woocommerce', | |
| 'Shared Subscriptions', | |
| 'Shared Subscriptions', | |
| self::CAPABILITY, | |
| 'wcss-shared-subscriptions', | |
| array( $this, 'render_admin_page' ) | |
| ); | |
| } | |
| public function render_admin_page(){ | |
| if( ! current_user_can( self::CAPABILITY ) ){ wp_die( 'Access denied.' ); } | |
| $sub_id = isset( $_GET['subscription_id'] ) ? absint( $_GET['subscription_id'] ) : 0; | |
| $subscription = $sub_id ? wcs_get_subscription( $sub_id ) : false; | |
| $members = array(); | |
| $owner_id = 0; | |
| if( $subscription ){ | |
| $owner_id = (int) $subscription->get_user_id(); | |
| $members = get_post_meta( $subscription->get_id(), self::META_KEY_MEMBERS, true ); | |
| if( ! is_array( $members ) ){ $members = array(); } | |
| } | |
| ?> | |
| <div class="wrap"> | |
| <h1>Shared Subscriptions</h1> | |
| <form method="get" action=""> | |
| <input type="hidden" name="page" value="wcss-shared-subscriptions" /> | |
| <p> | |
| <label for="wcss-sub-id">Subscription ID</label> | |
| <input type="number" min="1" id="wcss-sub-id" name="subscription_id" value="<?php echo esc_attr( $sub_id ); ?>" /> | |
| <?php submit_button( 'Load', 'secondary', '', false ); ?> | |
| </p> | |
| </form> | |
| <?php if( $subscription ): ?> | |
| <hr /> | |
| <h2>Subscription #<?php echo esc_html( $subscription->get_id() ); ?></h2> | |
| <p><strong>Owner account:</strong> <?php echo esc_html( $this->user_display( $owner_id ) ); ?></p> | |
| <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"> | |
| <?php wp_nonce_field( self::NONCE_ACTION ); ?> | |
| <input type="hidden" name="action" value="wcss_save_members" /> | |
| <input type="hidden" name="subscription_id" value="<?php echo esc_attr( $subscription->get_id() ); ?>" /> | |
| <table class="widefat striped"> | |
| <thead> | |
| <tr> | |
| <th style="width:40%">Members (user IDs or emails)</th> | |
| <th>Notes</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td> | |
| <textarea name="wcss_members" rows="6" style="width:100%" placeholder="Enter one per line"><?php | |
| $prefill = array(); | |
| foreach( $members as $uid ){ | |
| $u = get_user_by( 'id', (int) $uid ); | |
| $prefill[] = $u ? $u->user_email : (string) $uid; | |
| } | |
| echo esc_textarea( implode( "\n", $prefill ) ); | |
| ?></textarea> | |
| </td> | |
| <td> | |
| <p>Owner is included automatically. Owner cannot be removed here.</p> | |
| <p>Lines can be user IDs or emails. Non-existent emails are ignored.</p> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| <?php submit_button( 'Save Members' ); ?> | |
| </form> | |
| <?php elseif( $sub_id ): ?> | |
| <p>No subscription found.</p> | |
| <?php endif; ?> | |
| </div> | |
| <?php | |
| } | |
| private function user_display( $user_id ){ | |
| $u = get_user_by( 'id', (int) $user_id ); | |
| if( ! $u ){ return 'Unknown user'; } | |
| return sprintf( '%s (#%d, %s)', $u->display_name, $u->ID, $u->user_email ); | |
| } | |
| public function handle_save_members(){ | |
| if( ! current_user_can( self::CAPABILITY ) ){ wp_die( 'Access denied.' ); } | |
| check_admin_referer( self::NONCE_ACTION ); | |
| $sub_id = isset( $_POST['subscription_id'] ) ? absint( $_POST['subscription_id'] ) : 0; | |
| $subscription = $sub_id ? wcs_get_subscription( $sub_id ) : false; | |
| if( ! $subscription ){ wp_redirect( add_query_arg( array( 'page' => 'wcss-shared-subscriptions', 'subscription_id' => $sub_id, 'updated' => 0 ), admin_url( 'admin.php' ) ) ); exit; } | |
| $lines = isset( $_POST['wcss_members'] ) ? explode( "\n", wp_unslash( $_POST['wcss_members'] ) ) : array(); | |
| $clean_ids = array(); | |
| foreach( $lines as $raw ){ | |
| $raw = trim( $raw ); | |
| if( $raw === '' ){ continue; } | |
| if( is_numeric( $raw ) ){ | |
| $uid = absint( $raw ); | |
| if( $uid && get_user_by( 'id', $uid ) ){ $clean_ids[] = $uid; } | |
| continue; | |
| } | |
| if( is_email( $raw ) ){ | |
| $u = get_user_by( 'email', $raw ); | |
| if( $u ){ $clean_ids[] = (int) $u->ID; } | |
| } | |
| } | |
| $clean_ids = array_values( array_unique( array_filter( array_map( 'absint', $clean_ids ) ) ) ); | |
| // Ensure owner is not duplicated in members list | |
| $owner_id = (int) $subscription->get_user_id(); | |
| $clean_ids = array_values( array_diff( $clean_ids, array( $owner_id ) ) ); | |
| update_post_meta( $subscription->get_id(), self::META_KEY_MEMBERS, $clean_ids ); | |
| $this->reschedule_notices_generic( $subscription->get_id() ); | |
| wp_redirect( add_query_arg( array( 'page' => 'wcss-shared-subscriptions', 'subscription_id' => $subscription->get_id(), 'updated' => 1 ), admin_url( 'admin.php' ) ) ); | |
| exit; | |
| } | |
| /* ========================= | |
| * Permission Extensions | |
| * ========================= */ | |
| public function extend_view_caps( $can_view, $subscription_id ){ | |
| if( $can_view ){ return true; } | |
| $user_id = get_current_user_id(); | |
| if( ! $user_id ){ return $can_view; } | |
| return wcss_user_is_linked_to_subscription( $subscription_id, $user_id ); | |
| } | |
| public function allow_members_to_view_update( $can, $new_status, $subscription ){ | |
| // Keep default capability checks intact for updates; this ensures we don't over-grant mutating powers. | |
| return $can; | |
| } | |
| /* ========================= | |
| * Renewal Notice Scheduling | |
| * ========================= */ | |
| public static function activate(){ | |
| // Nothing mandatory; developers may bulk schedule via WP-CLI if needed. | |
| } | |
| public static function deactivate(){ | |
| // Clear all scheduled hooks for cleanliness. | |
| $crons = _get_cron_array(); | |
| if( is_array( $crons ) ){ | |
| foreach( $crons as $timestamp => $hooks ){ | |
| foreach( $hooks as $hook => $events ){ | |
| if( in_array( $hook, array( self::CRON_HOOK_BEFORE, self::CRON_HOOK_AFTER ), true ) ){ | |
| foreach( $events as $sig => $data ){ | |
| wp_unschedule_event( $timestamp, $hook, $data['args'] ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| public function reschedule_notices_on_status_change( $subscription, $old_status, $new_status ){ | |
| $this->reschedule_notices_generic( $subscription->get_id() ); | |
| } | |
| public function reschedule_notices_on_date_change( $subscription, $date_type, $datetime ){ | |
| $this->reschedule_notices_generic( $subscription->get_id() ); | |
| } | |
| public function reschedule_notices_generic( $subscription_id ){ | |
| $subscription = wcs_get_subscription( $subscription_id ); | |
| if( ! $subscription ){ return; } | |
| // Clear existing schedules for this subscription | |
| $this->clear_schedules_for( $subscription_id ); | |
| // Only schedule for active subscriptions with a next payment date | |
| if( $subscription->has_status( array( 'active', 'on-hold' ) ) ){ | |
| $next = $subscription->get_date( 'next_payment' ); | |
| if( $next ){ | |
| $ts_next = strtotime( $next ); | |
| if( $ts_next && $ts_next > 0 ){ | |
| $before = $ts_next - WEEK_IN_SECONDS; | |
| $after = $ts_next + WEEK_IN_SECONDS; | |
| if( $before > time() ){ | |
| wp_schedule_single_event( $before, self::CRON_HOOK_BEFORE, array( $subscription_id ) ); | |
| } | |
| if( $after > time() ){ | |
| wp_schedule_single_event( $after, self::CRON_HOOK_AFTER, array( $subscription_id ) ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private function clear_schedules_for( $subscription_id ){ | |
| // Unschedule any pending events matching our hooks for this subscription | |
| $hooks = array( self::CRON_HOOK_BEFORE, self::CRON_HOOK_AFTER ); | |
| foreach( $hooks as $hook ){ | |
| $next = wp_next_scheduled( $hook, array( $subscription_id ) ); | |
| while( $next ){ | |
| wp_unschedule_event( $next, $hook, array( $subscription_id ) ); | |
| $next = wp_next_scheduled( $hook, array( $subscription_id ) ); | |
| } | |
| } | |
| } | |
| public function emit_notice_before( $subscription_id ){ | |
| /** | |
| * Fires when a subscription is 1 week before renewal. | |
| * @param int $subscription_id | |
| * @param string $when 'before' | |
| */ | |
| do_action( self::ACTION_NOTICE, $subscription_id, 'before' ); | |
| } | |
| public function emit_notice_after( $subscription_id ){ | |
| /** | |
| * Fires when a subscription is 1 week after the scheduled renewal date. | |
| * @param int $subscription_id | |
| * @param string $when 'after' | |
| */ | |
| do_action( self::ACTION_NOTICE, $subscription_id, 'after' ); | |
| } | |
| /* ========================= | |
| * Default Listener (Example) | |
| * ========================= */ | |
| public function default_email_listener( $subscription_id, $when ){ | |
| $subscription = wcs_get_subscription( $subscription_id ); | |
| if( ! $subscription ){ return; } | |
| $owner_id = (int) $subscription->get_user_id(); | |
| $owner = get_user_by( 'id', $owner_id ); | |
| $members = get_post_meta( $subscription_id, self::META_KEY_MEMBERS, true ); | |
| if( ! is_array( $members ) ){ $members = array(); } | |
| $emails = array(); | |
| if( $owner && is_email( $owner->user_email ) ){ $emails[] = $owner->user_email; } | |
| foreach( $members as $uid ){ | |
| $u = get_user_by( 'id', (int) $uid ); | |
| if( $u && is_email( $u->user_email ) ){ $emails[] = $u->user_email; } | |
| } | |
| $emails = array_values( array_unique( array_filter( $emails ) ) ); | |
| if( empty( $emails ) ){ return; } | |
| $product_titles = array(); | |
| foreach( $subscription->get_items() as $item ){ | |
| $product_titles[] = $item->get_name(); | |
| } | |
| $products_str = implode( ', ', $product_titles ); | |
| $when_label = $when === 'before' ? 'Upcoming renewal' : 'Recent renewal'; | |
| $subject = apply_filters( 'wcss_email_subject', sprintf( '%s: Subscription #%d', $when_label, $subscription_id ), $subscription_id, $when ); | |
| $body = apply_filters( | |
| 'wcss_email_body', | |
| sprintf( | |
| "%s\n\nSubscription: #%d\nItems: %s\nNext payment: %s\nStatus: %s\n", | |
| $when_label, | |
| $subscription_id, | |
| $products_str, | |
| (string) $subscription->get_date( 'next_payment' ), | |
| $subscription->get_status() | |
| ), | |
| $subscription_id, | |
| $when | |
| ); | |
| /** | |
| * Filter recipients list for the default email listener. | |
| * @param array $emails | |
| * @param int $subscription_id | |
| * @param string $when | |
| */ | |
| $emails = apply_filters( 'wcss_email_recipients', $emails, $subscription_id, $when ); | |
| // Basic mail send. Replace or remove this listener and wire your own if you need HTML or WC mailer templates. | |
| foreach( $emails as $email ){ | |
| wp_mail( $email, $subject, $body ); | |
| } | |
| } | |
| } | |
| WCSS_Shared_Subscriptions::instance(); | |
| } | |
| /* ============================================================ | |
| * PUBLIC HELPERS (callable anywhere) | |
| * ============================================================ */ | |
| /** | |
| * Check if a user is linked to a subscription as owner or member. | |
| * Returns true/false. Strict existence check; does not check active status. | |
| */ | |
| if( ! function_exists( 'wcss_user_is_linked_to_subscription' ) ){ | |
| function wcss_user_is_linked_to_subscription( $subscription_id, $user_id = 0 ){ | |
| $user_id = $user_id ? (int) $user_id : get_current_user_id(); | |
| if( ! $user_id || ! function_exists( 'wcs_get_subscription' ) ){ return false; } | |
| $subscription = wcs_get_subscription( $subscription_id ); | |
| if( ! $subscription ){ return false; } | |
| if( (int) $subscription->get_user_id() === (int) $user_id ){ return true; } | |
| $members = get_post_meta( $subscription_id, WCSS_Shared_Subscriptions::META_KEY_MEMBERS, true ); | |
| if( ! is_array( $members ) ){ $members = array(); } | |
| return in_array( (int) $user_id, array_map( 'intval', $members ), true ); | |
| } | |
| } | |
| /** | |
| * Helper: is the current user linked (owner or member) AND is the subscription active/on-hold? | |
| * Use in theme/plugin code to gate benefits. | |
| */ | |
| if( ! function_exists( 'wcss_current_user_has_active_link' ) ){ | |
| function wcss_current_user_has_active_link( $subscription_id ){ | |
| if( ! function_exists( 'wcs_get_subscription' ) ){ return false; } | |
| $user_id = get_current_user_id(); | |
| if( ! $user_id ){ return false; } | |
| $linked = wcss_user_is_linked_to_subscription( $subscription_id, $user_id ); | |
| if( ! $linked ){ return false; } | |
| $subscription = wcs_get_subscription( $subscription_id ); | |
| return $subscription && $subscription->has_status( array( 'active', 'on-hold' ) ); | |
| } | |
| } | |
| /** | |
| * Helper: does the user have an active link to any subscription that contains a given product? | |
| * Optional utility if you gate access by product ownership. | |
| */ | |
| if( ! function_exists( 'wcss_user_has_access_to_product' ) ){ | |
| function wcss_user_has_access_to_product( $user_id, $product_id ){ | |
| if( ! function_exists( 'wcs_get_subscriptions' ) ){ return false; } | |
| $args = array( | |
| 'customer_id' => (int) $user_id, | |
| 'status' => array( 'active', 'on-hold' ), | |
| ); | |
| $subs = wcs_get_subscriptions( $args ); | |
| // Extend with subscriptions where $user_id is a member | |
| $member_subs = get_posts( array( | |
| 'post_type' => 'shop_subscription', | |
| 'post_status' => 'any', | |
| 'posts_per_page' => -1, | |
| 'meta_query' => array( | |
| array( | |
| 'key' => WCSS_Shared_Subscriptions::META_KEY_MEMBERS, | |
| 'value' => '"' . (int) $user_id . '"', | |
| 'compare' => 'LIKE', | |
| ), | |
| ), | |
| 'fields' => 'ids', | |
| ) ); | |
| foreach( $member_subs as $sid ){ | |
| $s = wcs_get_subscription( $sid ); | |
| if( $s && $s->has_status( array( 'active', 'on-hold' ) ) ){ | |
| $subs[ $sid ] = $s; | |
| } | |
| } | |
| if( empty( $subs ) ){ return false; } | |
| foreach( $subs as $sub ){ | |
| foreach( $sub->get_items() as $item ){ | |
| if( (int) $item->get_product_id() === (int) $product_id || (int) $item->get_variation_id() === (int) $product_id ){ | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| /** | |
| * Utility: get member user IDs array for a subscription. | |
| */ | |
| if( ! function_exists( 'wcss_get_subscription_members' ) ){ | |
| function wcss_get_subscription_members( $subscription_id ){ | |
| $members = get_post_meta( $subscription_id, WCSS_Shared_Subscriptions::META_KEY_MEMBERS, true ); | |
| return is_array( $members ) ? array_values( array_map( 'intval', $members ) ) : array(); | |
| } | |
| } | |
| /* ============================================================ | |
| * DEVELOPER NOTES | |
| * | |
| * - Assign linked users in WP-Admin: WooCommerce → Shared Subscriptions. | |
| * - Use wcss_user_is_linked_to_subscription( $subscription_id, $user_id ) to check linkage. | |
| * - Use wcss_current_user_has_active_link( $subscription_id ) to gate benefits. | |
| * - Renewal notice framework: | |
| * * Schedules two wp-cron events relative to next payment: -7 days and +7 days. | |
| * * Emits a single reusable action: do_action( 'wcss_subscription_renewal_notice', $subscription_id, 'before'|'after' ); | |
| * * Replace or augment the default listener if you prefer WooCommerce Mailer templates. | |
| * - Hooks you can use: | |
| * * add_action( 'wcss_subscription_renewal_notice', function( $subscription_id, $when ){ ... }, 10, 2 ); | |
| * * add_filter( 'wcss_email_subject', function( $subject, $subscription_id, $when ){ ... }, 10, 3 ); | |
| * * add_filter( 'wcss_email_body', function( $body, $subscription_id, $when ){ ... }, 10, 3 ); | |
| * * add_filter( 'wcss_email_recipients', function( $emails, $subscription_id, $when ){ ... }, 10, 3 ); | |
| * | |
| * - Safety: UI vocabulary avoids “Parent/Child.” It uses “Owner account” and “Members.” | |
| * ============================================================ */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment