Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions inc/managers/class-membership-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ function () {

add_action('wu_transition_membership_status', [$this, 'transition_membership_status'], 10, 3);

add_action('wu_transition_membership_status', [$this, 'handle_pending_site_on_cancellation'], 10, 3);

/*
* Deal with delayed/schedule swaps
*/
Expand Down Expand Up @@ -352,6 +354,56 @@ public function mark_cancelled_date($old_value, $new_value, $item_id): void {
}
}

/**
* Preserve pending_site data in a transient when a membership is cancelled.
*
* When a membership transitions to `cancelled`, any orphaned pending_site
* stored in membership meta is moved to a 24-hour transient keyed by the
* customer's email hash. This allows a retry checkout flow to reclaim the
* pending site rather than losing it permanently.
*
* Transient key format: `wu_transferable_pending_` . md5( $email )
*
* @since 2.3.2
*
* @param string $old_status The previous membership status.
* @param string $new_status The new membership status.
* @param int $membership_id The ID of the membership.
* @return void
*/
public function handle_pending_site_on_cancellation($old_status, $new_status, $membership_id): void {

if ('cancelled' !== $new_status) {
return;
}

$membership = wu_get_membership($membership_id);

if ( ! $membership) {
return;
}

$pending_site = $membership->get_pending_site();

if ( ! $pending_site) {
return;
}

$customer = $membership->get_customer();

if ( ! $customer) {
$membership->delete_pending_site();
return;
}

$email = $customer->get_email_address();
$transient_key = 'wu_transferable_pending_' . md5($email);

set_transient($transient_key, $pending_site, DAY_IN_SECONDS);

$membership->delete_pending_site();
}

/**
* Transfer a membership from a user to another.
*
Expand Down
128 changes: 128 additions & 0 deletions tests/WP_Ultimo/Managers/Membership_Manager_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -681,4 +681,132 @@ public function test_log_file_name_constant(): void {

$this->assertEquals('memberships', Membership_Manager::LOG_FILE_NAME);
}

// ========================================================================
// handle_pending_site_on_cancellation()
// ========================================================================

/**
* Test init registers the handle_pending_site_on_cancellation hook.
*/
public function test_init_registers_handle_pending_site_on_cancellation_hook(): void {

$manager = $this->get_manager_instance();

$this->assertIsInt(
has_action('wu_transition_membership_status', [$manager, 'handle_pending_site_on_cancellation'])
);
}

/**
* Test pending_site is deleted from membership meta on cancellation.
*/
public function test_handle_pending_site_on_cancellation_cleans_up_pending_site(): void {

$membership = $this->create_membership(['status' => Membership_Status::ACTIVE]);
$manager = $this->get_manager_instance();

$membership->create_pending_site(
[
'title' => 'Test Pending Site',
'path' => '/testpending/',
]
);

$this->assertNotFalse($membership->get_pending_site(), 'pending_site should exist before cancellation');

$manager->handle_pending_site_on_cancellation(
Membership_Status::ACTIVE,
Membership_Status::CANCELLED,
$membership->get_id()
);

$refreshed = wu_get_membership($membership->get_id());

$this->assertFalse($refreshed->get_pending_site(), 'pending_site should be removed after cancellation');
}

/**
* Test transient is set with the correct key and 24-hour TTL on cancellation.
*/
public function test_handle_pending_site_on_cancellation_sets_transient_with_correct_key(): void {

$membership = $this->create_membership(['status' => Membership_Status::ACTIVE]);
$manager = $this->get_manager_instance();

$membership->create_pending_site(
[
'title' => 'Transient Test Site',
'path' => '/transientpending/',
]
);

$email = $this->customer->get_email_address();
$transient_key = 'wu_transferable_pending_' . md5($email);

// Ensure transient does not exist before the call.
delete_transient($transient_key);
$this->assertFalse(get_transient($transient_key), 'transient should not exist before cancellation');

$manager->handle_pending_site_on_cancellation(
Membership_Status::ACTIVE,
Membership_Status::CANCELLED,
$membership->get_id()
);

$stored = get_transient($transient_key);

$this->assertNotFalse($stored, 'transient should be set after cancellation');
}

/**
* Test no transient is set and no error occurs when there is no pending_site.
*/
public function test_handle_pending_site_on_cancellation_skips_when_no_pending_site(): void {

$membership = $this->create_membership(['status' => Membership_Status::ACTIVE]);
$manager = $this->get_manager_instance();

// Confirm no pending site exists.
$this->assertFalse($membership->get_pending_site());

$email = $this->customer->get_email_address();
$transient_key = 'wu_transferable_pending_' . md5($email);
delete_transient($transient_key);

$manager->handle_pending_site_on_cancellation(
Membership_Status::ACTIVE,
Membership_Status::CANCELLED,
$membership->get_id()
);

$this->assertFalse(get_transient($transient_key), 'transient should NOT be set when there is no pending_site');
}

/**
* Test non-cancellation transitions do not affect pending_site.
*/
public function test_handle_pending_site_on_cancellation_ignores_non_cancelled_status(): void {

$membership = $this->create_membership(['status' => Membership_Status::ACTIVE]);
$manager = $this->get_manager_instance();

$membership->create_pending_site(
[
'title' => 'Should Stay',
'path' => '/shouldstay/',
]
);

// Trigger with a non-cancelled new status.
$manager->handle_pending_site_on_cancellation(
Membership_Status::ACTIVE,
Membership_Status::ON_HOLD,
$membership->get_id()
);

$refreshed = wu_get_membership($membership->get_id());

$this->assertNotFalse($refreshed->get_pending_site(), 'pending_site should remain intact for non-cancelled transitions');
}
}
Loading