Created
March 9, 2026 19:43
-
-
Save patric-boehner/c600a11696ee765e11f6c5ac93ee79c1 to your computer and use it in GitHub Desktop.
Auto-extend membership expiration and send receipt email for check orders in PMPRO
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 | |
| /** | |
| * Auto-extend membership expiration and send receipt email | |
| * when an admin manually creates a check order. | |
| * | |
| * New orders fire `pmpro_added_order` (not `pmpro_update_order`), | |
| * so the pay-by-check plugin's logic never runs for them. | |
| * This fills that gap for manually created check orders. | |
| * | |
| * End date calculation mirrors PMPro core's pmpro_checkout_level_extend_memberships(): | |
| * if the member has an active membership with time remaining, the new expiration is | |
| * calculated from their current end date (preserving unused time). For expired or | |
| * new members, it is calculated from today. | |
| */ | |
| add_action( 'pmpro_added_order', 'dor_handle_manual_check_order' ); | |
| function dor_handle_manual_check_order( $morder ) { | |
| // Only handle check gateway orders. | |
| if ( 'check' !== strtolower( $morder->gateway ) ) { | |
| return; | |
| } | |
| // Only handle orders with 'success' status. | |
| if ( 'success' !== $morder->status ) { | |
| return; | |
| } | |
| // Require valid user and membership level. | |
| if ( empty( $morder->user_id ) || empty( $morder->membership_id ) ) { | |
| return; | |
| } | |
| $user_id = intval( $morder->user_id ); | |
| $membership_id = intval( $morder->membership_id ); | |
| // Get the level configuration. | |
| $level = pmpro_getLevel( $membership_id ); | |
| if ( empty( $level ) ) { | |
| return; | |
| } | |
| // Use expiration settings if configured, otherwise fall back to billing cycle | |
| // settings (mirrors what the Auto-Renewal Checkbox plugin does at checkout). | |
| $exp_number = ! empty( $level->expiration_number ) ? $level->expiration_number : $level->cycle_number; | |
| $exp_period = ! empty( $level->expiration_period ) ? $level->expiration_period : $level->cycle_period; | |
| if ( empty( $exp_number ) || empty( $exp_period ) ) { | |
| return; | |
| } | |
| // Check if the user currently has an active membership for this level. | |
| // Done here (before end date calculation) so we can use their existing | |
| // expiration date as the base — mirroring pmpro_checkout_level_extend_memberships() | |
| // in PMPro core, which carries over remaining time during checkout renewals. | |
| $current_membership = pmpro_getSpecificMembershipLevelForUser( $user_id, $membership_id ); | |
| // Base date: if the member is active with a future expiration, extend from | |
| // that date so remaining membership time is preserved. Otherwise use today. | |
| $base_timestamp = current_time( 'timestamp' ); | |
| if ( ! empty( $current_membership ) && ! empty( $current_membership->enddate ) && $current_membership->enddate > $base_timestamp ) { | |
| $base_timestamp = $current_membership->enddate; | |
| } | |
| if ( 'Hour' === $exp_period ) { | |
| $enddate = date( 'Y-m-d H:i:s', strtotime( '+ ' . $exp_number . ' ' . $exp_period, $base_timestamp ) ); | |
| } else { | |
| $enddate = date( 'Y-m-d 23:59:59', strtotime( '+ ' . $exp_number . ' ' . $exp_period, $base_timestamp ) ); | |
| } | |
| /** | |
| * Filter the new expiration date for manual check orders. | |
| * | |
| * @param string $enddate The calculated expiration date (Y-m-d H:i:s format). | |
| * @param int $user_id The user ID. | |
| * @param int $membership_id The membership level ID. | |
| * @param object $morder The order object. | |
| */ | |
| $enddate = apply_filters( 'dor_manual_check_order_enddate', $enddate, $user_id, $membership_id, $morder ); | |
| if ( ! empty( $current_membership ) ) { | |
| // Active member — update the expiration date (preserves start date). | |
| pmpro_set_expiration_date( $user_id, $membership_id, $enddate ); | |
| // Queue a Mailchimp sync so END_DATE and other merge fields update. | |
| // pmpro_set_expiration_date() is a direct DB write with no hooks, so the | |
| // Mailchimp addon never learns about the new end date. The expired/non-member | |
| // path below doesn't need this — pmpro_changeMembershipLevel() already fires | |
| // pmpro_after_change_membership_level, which the addon listens to. | |
| if ( function_exists( 'pmpromc_queue_subscription' ) ) { | |
| $mc_options = get_option( 'pmpromc_options' ); | |
| if ( ! empty( $mc_options ) ) { | |
| if ( ! empty( $mc_options[ 'level_' . $membership_id . '_lists' ] ) ) { | |
| pmpromc_queue_subscription( $user_id, $mc_options[ 'level_' . $membership_id . '_lists' ] ); | |
| } | |
| if ( ! empty( $mc_options['users_lists'] ) ) { | |
| pmpromc_queue_subscription( $user_id, $mc_options['users_lists'] ); | |
| } | |
| } | |
| } | |
| } else { | |
| // Expired or non-member — reinstate/grant membership. | |
| $custom_level = array( | |
| 'user_id' => $user_id, | |
| 'membership_id' => $membership_id, | |
| 'initial_payment' => $level->initial_payment, | |
| 'billing_amount' => 0, | |
| 'cycle_number' => 0, | |
| 'cycle_period' => 'Month', | |
| 'billing_limit' => 0, | |
| 'trial_amount' => 0, | |
| 'trial_limit' => 0, | |
| 'startdate' => current_time( 'mysql' ), | |
| 'enddate' => $enddate, | |
| ); | |
| pmpro_changeMembershipLevel( $custom_level, $user_id, 'admin_changed' ); | |
| } | |
| // Send invoice/receipt email to the member. | |
| $recipient = get_user_by( 'ID', $user_id ); | |
| if ( $recipient ) { | |
| $invoice_email = new PMProEmail(); | |
| $invoice_email->sendInvoiceEmail( $recipient, $morder ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment