Subscription Purchase - Asynchronous Handling

Overview

When a subscription payment is completed, the system asynchronously initializes several key attributes:

  • Copies owed (number of issues the subscriber is entitled to)
  • Deferred amount (revenue to be recognized over the subscription term)
  • Issue shipping basis (how issues are determined for shipping)
  • Shipping information (address and soundex for duplicate detection)

These are handled asynchronously outside of the standard subscription flow to avoid delaying checkout and therefore improving the customer experience.

Initialization Flow

1. Payment Completion Hook

The process begins when a payment is completed for a new subscription:

// Hook into payment completion
add_action('woocommerce_subscription_payment_complete', 'subscription_payment_complete', 30, 1);

2. Asynchronous Action Scheduling

Our subscription controller schedules an asynchronous action to handle the initialization:

public function subscription_payment_complete($subscription) {
    // Only process original orders, not renewals
    if ($subscription->get_payment_count() > 1 || false === $subscription->get_parent()) {
        return;
    }
    
    // Schedule async action with 20 second delay
    as_schedule_single_action(
        time() + 20, 
        'jc_new_subscription_payment_complete_async', 
        ['subscription_id' => $subscription->get_id()], 
        'jc_subscriptions'
    );
}

3. Hook Registration

The async action handler for the subscription initialization is registered on the scheduled action (which will fire at least 20 seconds after subscription purchase):

// Register the async action handler
$this->loader->add_action(
    'jc_new_subscription_payment_complete_async', 
    $subscription_controller, 
    'new_subscription_payment_complete', 
    10, 
    1
);

The new_subscription_payment_complete function handles the initialization.

public function new_subscription_payment_complete( $subscription_id ) {

    $tsj_subscription = tsj_subscriptions_get_subscription( $subscription_id );
    $wc_subscription = wcs_get_subscription( $subscription_id );

    /**
     * Check if subscription is really new -- we've had some renewals come through as new (caching?)
     */
    if ( $tsj_subscription->get_start_issue_number() != 0 ) {
        wc_get_logger()->error( 
            'Subscription already initialised - not a new subscription #' . $subscription_id . ' start issue number ' . $tsj_subscription->get_start_issue_number(), ['source' => 'Journal Subscriptions'] 
        );

        return;
    }
    ...

4. Copies Owed Calculation

The number of copies owed is calculated:

// Increment copies owed by issues per term
$number_issues = $tsj_subscription->get_issues_per_term();

if ( $tsj_subscription->get_includes_current_issue() ) {
    $number_issues++;
}

$tsj_subscription->set_copies_owed( apply_filters( 'tsj_subscription_copies_owed', $number_issues, $tsj_subscription ) );

5. Deferred Amount Assignment

The deferred amount is set:

$parent = $wc_subscription->get_parent();

$deferred_amount = $parent->get_total();

$tsj_subscription->set_deferred_amount( $deferred_amount );

Additional tasks such as setting the shipping soundex & address keys, premium status and sku are performed.

6. Post-Initialization Tasks

After saving the subscription, additional async actions are scheduled. Expiry group & entitlement handling are hooked on this further async action:

// Save the subscription
$tsj_subscription->save();

...

// Schedule post-initialization tasks
as_enqueue_async_action(
    'journal_subscription_created',
    [
        'tsj_subscription' => $tsj_subscription->get_id(),
        'notes' => $notes
    ],
    'jc_subscriptions'
);