Skip to content

Instantly share code, notes, and snippets.

@patric-boehner
Created March 9, 2026 19:43
Show Gist options
  • Select an option

  • Save patric-boehner/c600a11696ee765e11f6c5ac93ee79c1 to your computer and use it in GitHub Desktop.

Select an option

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
<?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